/*
* © Copyright FOCONIS AG, 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
*/
package org.openntf.formula.function;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openntf.formula.DateTime;
import org.openntf.formula.Formatter;
import org.openntf.formula.Formatter.LotusDateTimeOptions;
import org.openntf.formula.FormulaContext;
import org.openntf.formula.FormulaParseException;
import org.openntf.formula.FormulaParser;
import org.openntf.formula.Formulas;
import org.openntf.formula.Function;
import org.openntf.formula.FunctionFactory;
import org.openntf.formula.FunctionSet;
import org.openntf.formula.ValueHolder;
import org.openntf.formula.ValueHolder.DataType;
import org.openntf.formula.annotation.DiffersFromLotus;
import org.openntf.formula.annotation.OpenNTF;
import org.openntf.formula.annotation.ParamCount;
public enum TextFunctions {
;
public static class Functions extends FunctionSet {
private static final Map<String, Function> functionSet = FunctionFactory.getFunctions(TextFunctions.class);
@Override
public Map<String, Function> getFunctions() {
return functionSet;
}
}
/*----------------------------------------------------------------------------*/
/*
* @Left, @LeftBack, @Right, @RightBack
*/
/*----------------------------------------------------------------------------*/
private static Integer numOrStringTest(final Object what, final boolean mustBeNonNeg) {
if (what instanceof Number) {
int nWhat = ((Number) what).intValue();
if (nWhat < 0 && mustBeNonNeg)
throw new IllegalArgumentException("Got negative Number: " + what.toString());
return (nWhat);
}
if (what instanceof String)
return (null);
throw new IllegalArgumentException("Expected Number or String, got " + what.getClass());
}
/*----------------------------------------------------------------------------*/
private static String leftRight(final String whose, final Object what, final boolean left, final boolean back) {
int lh = whose.length();
int howMany;
Integer i = numOrStringTest(what, true);
if (i != null) {
if ((howMany = i) > lh)
howMany = lh;
if (back)
howMany = lh - howMany;
} else {
String sWhat = (String) what;
if (back)
howMany = whose.lastIndexOf(sWhat);
else
howMany = whose.indexOf(sWhat);
if (howMany < 0)
return "";
if (!left)
howMany = lh - howMany - sWhat.length();
}
return left ? whose.substring(0, howMany) : whose.substring(lh - howMany);
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static String atLeft(final String whose, final Object what) {
return leftRight(whose, what, true, false);
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static String atLeftBack(final String whose, final Object what) {
return leftRight(whose, what, true, true);
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static String atRight(final String whose, final Object what) {
return leftRight(whose, what, false, false);
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static String atRightBack(final String whose, final Object what) {
return leftRight(whose, what, false, true);
}
/*----------------------------------------------------------------------------*/
/*
* @Middle, @MiddleBack
*/
/*----------------------------------------------------------------------------*/
private static String middle(final String whose, final Object what1, final Object what2, final boolean back) {
Integer num = numOrStringTest(what1, true);
int offset = (num == null) ? -1 : num;
int extraOffset1 = 0;
int lh = whose.length();
if (offset >= 0) {
if (back) {
offset = lh - offset;
if (offset < 0)
offset = 0;
else if (offset < lh)
offset++;
} else {
if (offset > lh)
offset = lh;
}
} else {
if (back)
offset = whose.lastIndexOf((String) what1);
else
offset = whose.indexOf((String) what1);
if (offset < 0)
return "";
extraOffset1 = ((String) what1).length();
}
int[] v = new int[2];
v[0] = offset;
v[1] = extraOffset1;
num = numOrStringTest(what2, false);
if (num != null)
calcMiddle(whose, num, back, v);
else
calcMiddle(whose, (String) what2, back, v);
offset = v[0];
int howMany = v[1];
if (offset < 0 || howMany <= 0 || offset >= lh)
return "";
if (howMany > lh - offset)
howMany = lh - offset;
return whose.substring(offset, offset + howMany);
}
/*----------------------------------------------------------------------------*/
private static void calcMiddle(final String whose, int nWhat2, final boolean back, final int[] v) {
int offset = v[0];
int extraOffset1 = v[1];
if (nWhat2 < 0) {
int oldOffset = offset;
nWhat2 = -nWhat2;
offset -= nWhat2;
if (offset < 0) {
offset = 0;
nWhat2 = oldOffset;
}
} else
offset += extraOffset1;
v[0] = offset;
v[1] = nWhat2;
}
/*----------------------------------------------------------------------------*/
private static void calcMiddle(final String whose, final String sWhat2, final boolean back, final int[] v) {
int offset = v[0];
int extraOffset1 = v[1];
int i;
int howMany;
if (back)
i = whose.substring(0, offset).lastIndexOf(sWhat2);
else
i = whose.substring(offset + extraOffset1).indexOf(sWhat2);
int extra = 0;
if (i < 0)
i = back ? 0 : whose.length();
else
extra = sWhat2.length();
if (back) {
howMany = offset;
offset = i + extra;
howMany -= offset;
} else {
offset += extraOffset1;
howMany = i;
}
v[0] = offset;
v[1] = howMany;
}
/*----------------------------------------------------------------------------*/
@ParamCount(3)
public static String atMiddle(final String whose, final Object what1, final Object what2) {
return middle(whose, what1, what2, false);
}
/*----------------------------------------------------------------------------*/
@ParamCount(3)
public static String atMiddleBack(final String whose, final Object what1, final Object what2) {
return middle(whose, what1, what2, true);
}
/*----------------------------------------------------------------------------*/
/*
* @Begins, @Ends, @Contains
*/
/*----------------------------------------------------------------------------*/
private static ValueHolder begEndCont(final FormulaContext ctx, final ValueHolder[] params, final char fkt) {
ValueHolder vh1 = params[0];
ValueHolder vh2 = params[1];
for (int i1 = 0; i1 < vh1.size; i1++) {
String what = vh1.getString(i1);
for (int i2 = 0; i2 < vh2.size; i2++) {
String how = vh2.getString(i2);
if ((fkt == 'b' && what.startsWith(how)) // @Begins
|| (fkt == 'e' && what.endsWith(how)) // @Ends
|| (fkt == 'c' && what.contains(how))) { // @Contains
return ctx.TRUE;
}
}
}
return ctx.FALSE;
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static ValueHolder atBegins(final FormulaContext ctx, final ValueHolder[] params) {
return begEndCont(ctx, params, 'b');
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static ValueHolder atEnds(final FormulaContext ctx, final ValueHolder[] params) {
return begEndCont(ctx, params, 'e');
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static ValueHolder atContains(final FormulaContext ctx, final ValueHolder[] params) {
return begEndCont(ctx, params, 'c');
}
/*----------------------------------------------------------------------------*/
/*
* @LowerCase, @UpperCase, @ProperCase
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static String atLowerCase(final String what) {
return what.toLowerCase();
}
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static String atUpperCase(final String what) {
return what.toUpperCase();
}
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static String atProperCase(final String what) {
int lh = what.length();
if (lh == 0)
return what;
char[] cArr = new char[lh];
boolean lastWasLetter = false;
for (int i = 0; i < lh; i++) {
char c = what.charAt(i);
if (Character.isLetter(c)) {
c = lastWasLetter ? Character.toLowerCase(c) : Character.toUpperCase(c);
lastWasLetter = true;
} else
lastWasLetter = false;
cArr[i] = c;
}
return new String(cArr);
}
/*----------------------------------------------------------------------------*/
/*
* @Char, @NewLine
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static String atChar(final Number n) {
int what = n.intValue();
if (what < 0 || what > 255)
throw new IllegalArgumentException("Expected Number between 0 and 255, got " + n.toString());
char[] cArr = new char[1];
cArr[0] = (char) what;
return new String(cArr);
}
@ParamCount(0)
public static ValueHolder atNewLine(final FormulaContext ctx) {
return ctx.NEWLINE;
}
/*----------------------------------------------------------------------------*/
/*
* @Count, @Elements, @Length, @Trim
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atCount(final ValueHolder[] params) {
return ValueHolder.valueOf(params[0].size);
}
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atElements(final ValueHolder[] params) {
int res = params[0].size;
if (res == 1 && params[0].dataType == DataType.STRING && params[0].getString(0).isEmpty())
res = 0;
return ValueHolder.valueOf(res);
}
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static int atLength(final String whose) {
return whose.length();
}
/*----------------------------------------------------------------------------*/
@DiffersFromLotus("Lotus doesn't trim tabs")
@ParamCount(1)
public static String atTrim(final String whom) {
int lh = whom.length();
char[] buff = new char[lh + 1];
boolean lastWasSpace = true;
int filled = 0;
for (int i = 0; i < lh; i++) {
char c = whom.charAt(i);
if (Character.isWhitespace(c))
lastWasSpace = true;
else {
if (lastWasSpace && filled != 0)
buff[filled++] = ' ';
buff[filled++] = c;
lastWasSpace = false;
}
}
return (filled == lh) ? whom : new String(buff, 0, filled);
}
/*----------------------------------------------------------------------------*/
/*
* @Explode, @Implode
*/
/*----------------------------------------------------------------------------*/
@ParamCount({ 1, 4 })
public static ValueHolder atExplode(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder vh = params[0];
String seps = " ,;";
boolean includeEmpties = false;
boolean newLineIsSep = true;
if (params.length >= 2)
seps = params[1].getString(0);
if (params.length >= 3)
includeEmpties = params[2].getBoolean(0);
if (params.length >= 4)
newLineIsSep = params[3].getBoolean(0);
if (newLineIsSep)
seps += "\n";
if (seps.isEmpty())
return vh;
Vector<String> res = new Vector<String>(100, 100);
for (int i = 0; i < vh.size; i++)
explodeOne(res, vh.getString(i), seps, includeEmpties);
int sz = res.size();
ValueHolder ret = ValueHolder.createValueHolder(String.class, sz);
for (int i = 0; i < sz; i++)
ret.add(res.get(i));
return ret;
}
/*----------------------------------------------------------------------------*/
private static void explodeOne(final Vector<String> res, final String what, final String seps, final boolean includeEmpties) {
int begin = 0;
int end;
int lh = what.length();
for (end = 0; end < lh; end++) {
char c = what.charAt(end);
if (seps.indexOf(c) < 0)
continue;
int korrEnd = end;
if (c == '\n' && end > 0 && what.charAt(end - 1) == '\r' && end > begin)
korrEnd--;
if (begin < korrEnd || includeEmpties)
res.add(what.substring(begin, korrEnd));
begin = end + 1;
}
if (begin < end || includeEmpties)
res.add(what.substring(begin, end));
}
/*----------------------------------------------------------------------------*/
@ParamCount({ 1, 2 })
public static ValueHolder atImplode(final ValueHolder[] params) {
String sep = (params.length == 1) ? " " : params[1].getString(0);
ValueHolder vh = params[0];
int bedarf = (vh.size - 1) * sep.length();
for (int i = 0; i < vh.size; i++)
bedarf += vh.getString(i).length();
StringBuilder sb = new StringBuilder(bedarf);
boolean first = true;
for (int i = 0; i < vh.size; i++) {
if (first)
first = false;
else
sb.append(sep);
sb.append(vh.getString(i));
}
return ValueHolder.valueOf(sb.toString());
}
/*----------------------------------------------------------------------------*/
/*
* @Word
*/
/*----------------------------------------------------------------------------*/
@ParamCount(3)
public static ValueHolder atWord(final ValueHolder[] params) {
ValueHolder vhSep = ValueHolder.valueOf(params[1].getString(0));
int which = params[2].getInt(0);
ValueHolder vh = params[0];
ValueHolder ret = ValueHolder.createValueHolder(String.class, vh.size);
Vector<String> aux = new Vector<String>(100, 100);
for (int i = 0; i < vh.size; i++)
word4One(ret, aux, vh.getString(i), vhSep, which);
return ret;
}
/*----------------------------------------------------------------------------*/
private static void word4One(final ValueHolder ret, final Vector<String> aux, final String whose, final ValueHolder vhSep, int which) {
aux.clear();
simpleSplitOne(aux, whose, vhSep, true);
int sz = aux.size();
String w;
if (which >= 0) {
if (which > 0)
which--;
w = (which >= sz) ? "" : aux.elementAt(which);
} else {
which = sz + which;
w = (which >= 0) ? aux.elementAt(which) : "";
}
ret.add(w);
}
/*----------------------------------------------------------------------------*/
/*
* @SplitSimple, @SplitRegExp
*/
/*----------------------------------------------------------------------------*/
/**
* Similar to @Explode, but respects the whole splitting strings. That is, every string in params[0] will be split along every string in
* params[1]. The optional parameter params[2] may contain the option [NOEMPTIES] (case-insensitive) to tell the method not to include
* empty splits in the result set.
*
* @return same as @Explode
*/
@OpenNTF
@ParamCount({ 2, 3 })
public static ValueHolder atSplitSimple(final ValueHolder[] params) {
boolean noEmpties = false;
if (params.length == 3) {
ValueHolder options = params[2];
for (int i = 0; i < options.size; i++) {
String opt = options.getString(i);
if ("[NOEMPTIES]".equalsIgnoreCase(opt))
noEmpties = true;
else
throw new IllegalArgumentException("Illegal Option: " + opt);
}
}
ValueHolder vh = params[0];
Vector<String> res = new Vector<String>(100, 100);
for (int i = 0; i < vh.size; i++)
simpleSplitOne(res, vh.getString(i), params[1], !noEmpties);
int sz = res.size();
ValueHolder ret = ValueHolder.createValueHolder(String.class, sz);
for (int i = 0; i < sz; i++)
ret.add(res.get(i));
return ret;
}
/*----------------------------------------------------------------------------*/
private static void simpleSplitOne(final Vector<String> res, final String whom, final ValueHolder sepStrings,
final boolean includeEmpties) {
int pos = 0;
int lh = whom.length();
while (pos <= lh) {
int found = -1;
int minIndex = lh + 1;
for (int i = 0; i < sepStrings.size; i++) {
int ind = whom.indexOf(sepStrings.getString(i), pos);
if (ind >= 0 && ind < minIndex) {
found = i;
minIndex = ind;
}
}
if (found == -1) {
if (pos < lh)
res.add(whom.substring(pos));
else if (includeEmpties)
res.add("");
break;
}
res.add(whom.substring(pos, minIndex));
pos = minIndex + sepStrings.getString(found).length();
}
}
/*----------------------------------------------------------------------------*/
/**
* Similar to @SplitSimple, but acts like String.split. That is, every string in params[0] will be split along the corresponding regular
* expression in params[1]. The optional parameter params[2] may contain the option [NOEMPTIES] (case-insensitive) to tell the method
* not to include empty splits in the result set.
*
* @return same as @Explode
*/
@OpenNTF
@ParamCount({ 2, 3 })
public static ValueHolder atSplitRegExp(final ValueHolder[] params) {
boolean noEmpties = false;
if (params.length == 3) {
ValueHolder options = params[2];
for (int i = 0; i < options.size; i++) {
String opt = options.getString(i);
if ("[NOEMPTIES]".equalsIgnoreCase(opt))
noEmpties = true;
else
throw new IllegalArgumentException("Illegal Option: " + opt);
}
}
ValueHolder vh = params[0];
ValueHolder regs = params[1];
String reg = null;
Vector<String> res = new Vector<String>(100, 100);
for (int i = 0; i < vh.size; i++) {
if (i < regs.size)
reg = regs.getString(i);
regExpSplitOne(res, vh.getString(i), reg, !noEmpties);
}
int sz = res.size();
ValueHolder ret = ValueHolder.createValueHolder(String.class, sz);
for (int i = 0; i < sz; i++)
ret.add(res.get(i));
return ret;
}
/*----------------------------------------------------------------------------*/
private static void regExpSplitOne(final Vector<String> res, final String which, final String reg, final boolean includeEmpties) {
String[] splits = which.split(reg);
for (int j = 0; j < splits.length; j++)
if (!splits[j].isEmpty() || includeEmpties)
res.add(splits[j]);
}
/*----------------------------------------------------------------------------*/
/*
* @IsNull, @IsNumber, @IsText, @IsTime
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atIsNull(final FormulaContext ctx, final ValueHolder[] params) {
Integer i = atElements(params).getInt(0);
return (i == 0) ? ctx.TRUE : ctx.FALSE;
}
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atIsNumber(final FormulaContext ctx, final ValueHolder[] params) {
return params[0].dataType.numeric ? ctx.TRUE : ctx.FALSE;
}
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atIsText(final FormulaContext ctx, final ValueHolder[] params) {
return (params[0].dataType == DataType.STRING) ? ctx.TRUE : ctx.FALSE;
}
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atIsTime(final FormulaContext ctx, final ValueHolder[] params) {
return (params[0].dataType == DataType.DATETIME) ? ctx.TRUE : ctx.FALSE;
}
/*----------------------------------------------------------------------------*/
/*
* @Failure
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static String atFailure(final String cause) {
return cause;
}
/*----------------------------------------------------------------------------*/
/*
* @ToNumber, @TextToNumber
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atToNumber(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder vh = params[0];
if (vh.dataType.numeric)
return vh;
if (vh.dataType != DataType.STRING)
throw new IllegalArgumentException("String or Number expected");
return (strToNumber(ctx, vh, true));
}
/*----------------------------------------------------------------------------*/
@DiffersFromLotus("Lotus gives e.g. @TextToNumber(\"1\":\"a\":\"2\")=1:@ERROR:2")
@ParamCount(1)
public static ValueHolder atTextToNumber(final FormulaContext ctx, final ValueHolder[] params) {
return strToNumber(ctx, params[0], false);
}
/*----------------------------------------------------------------------------*/
@SuppressWarnings("deprecation")
private static ValueHolder strToNumber(final FormulaContext ctx, final ValueHolder vh, final boolean emptySpecial) {
ValueHolder ret = ValueHolder.createValueHolder(Double.class, vh.size);
String val = null;
for (int i = 0; i < vh.size; i++) {
val = vh.getString(i);
if (val.isEmpty() && emptySpecial)
val = "0";
ret.add(ctx.getFormatter().parseNumber(val, true)); // lenient=true
}
return ret;
}
/*----------------------------------------------------------------------------*/
/*
* @ToTime, @TextToTime
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atToTime(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder vh = params[0];
if (vh.dataType == DataType.DATETIME)
return vh;
if (vh.dataType != DataType.STRING)
throw new IllegalArgumentException("String or DateTime expected");
return strToDateTime(ctx, vh);
}
/*----------------------------------------------------------------------------*/
@DiffersFromLotus("Lotus gives e.g. @IsError(@TextToTime(\"Nonsense\"))=0")
@ParamCount(1)
public static ValueHolder atTextToTime(final FormulaContext ctx, final ValueHolder[] params) {
return strToDateTime(ctx, params[0]);
}
/*----------------------------------------------------------------------------*/
private static ValueHolder strToDateTime(final FormulaContext ctx, final ValueHolder vh) {
ValueHolder ret = ValueHolder.createValueHolder(DateTime.class, vh.size);
Formatter formatter = ctx.getFormatter();
String val = null;
for (int i = 0; i < vh.size; i++) {
val = vh.getString(i);
ret.add(formatter.parseDate(val, true));
}
return ret;
}
/*----------------------------------------------------------------------------*/
/*
* @Keywords
*/
/*----------------------------------------------------------------------------*/
@ParamCount({ 2, 3 })
public static ValueHolder atKeywords(final ValueHolder[] params) {
boolean explicitSeps = (params.length == 3);
String seps = explicitSeps ? params[2].getString(0) : "?. \t\r\n,!;:[](){}\"<>";
Vector<String> res = new Vector<String>(params[1].size + 1);
if (seps.isEmpty())
keywordsNoSeps(res, params[0], params[1]);
else
keywordsWithSeps(res, seps, explicitSeps, params[0], params[1]);
int sz = res.size();
ValueHolder ret = ValueHolder.createValueHolder(String.class, sz);
for (int i = 0; i < sz; i++)
ret.add(res.get(i));
return ret;
}
/*----------------------------------------------------------------------------*/
private static void keywordsNoSeps(final Vector<String> res, final ValueHolder testStrs, final ValueHolder keywords) {
for (int i = 0; i < keywords.size; i++) {
String keyword = keywords.getString(i);
for (int j = 0; j < testStrs.size; j++) {
if (testStrs.getString(j).indexOf(keyword) >= 0) {
res.add(keyword);
break;
}
}
}
}
/*----------------------------------------------------------------------------*/
private static void keywordsWithSeps(final Vector<String> res, final String seps, final boolean explicitSeps,
final ValueHolder testStrs, final ValueHolder keywords) {
int bedarf = testStrs.size + 1;
for (int i = 0; i < testStrs.size; i++)
bedarf += testStrs.getString(i).length();
char[] buffer = new char[bedarf + 10];
int count = 0;
for (int i = 0; i < testStrs.size; i++) {
String ts = testStrs.getString(i);
int lh = ts.length();
boolean putIt = !explicitSeps;
boolean lastWasSep = true;
for (int j = 0; j < lh; j++) {
char c = ts.charAt(j);
if (seps.indexOf(c) >= 0) {
if (!lastWasSep)
buffer[count++] = (char) 0;
lastWasSep = true;
putIt = true;
continue;
}
if (putIt) {
buffer[count++] = c;
lastWasSep = false;
}
}
if (!lastWasSep)
buffer[count++] = (char) 0;
}
String testStr = new String(buffer, 0, count);
for (int i = 0; i < keywords.size; i++) {
String keyword = keywords.getString(i);
if (testStr.indexOf(keyword) >= 0)
res.add(keyword);
}
}
/*----------------------------------------------------------------------------*/
/*
* @Matches, @Like, @MatchesRegExp
*/
/*----------------------------------------------------------------------------*/
@DiffersFromLotus("Logical operations on patterns using &,|,! aren't yet supported")
@ParamCount(2)
public static ValueHolder atMatches(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder vhTester = params[0];
ValueHolder vhPatterns = params[1];
for (int ip = 0; ip < vhPatterns.size; ip++) {
TextMatchLotus tml = new TextMatchLotus(vhPatterns.getString(ip));
for (int it = 0; it < vhTester.size; it++)
if (tml.matches(vhTester.getString(it)))
return ctx.TRUE;
}
return ctx.FALSE;
}
/*----------------------------------------------------------------------------*/
@ParamCount({ 2, 3 })
public static ValueHolder atLike(final FormulaContext ctx, final ValueHolder[] params) {
char escape = 0;
if (params.length == 3) {
String e = params[2].getString(0);
if (e != null && e.length() != 0)
escape = e.charAt(0);
}
ValueHolder vhTester = params[0];
ValueHolder vhPatterns = params[1];
for (int ip = 0; ip < vhPatterns.size; ip++) {
TextMatchLotus tml = new TextMatchLotus(vhPatterns.getString(ip), escape);
for (int it = 0; it < vhTester.size; it++)
if (tml.matches(vhTester.getString(it)))
return ctx.TRUE;
}
return ctx.FALSE;
}
/*----------------------------------------------------------------------------*/
@OpenNTF
@ParamCount(2)
public static ValueHolder atMatchesRegExp(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder vhTester = params[0];
ValueHolder vhPatterns = params[1];
for (int ip = 0; ip < vhPatterns.size; ip++) {
Pattern regPatt = Pattern.compile(vhPatterns.getString(ip));
for (int it = 0; it < vhTester.size; it++)
if (regPatt.matcher(vhTester.getString(it)).matches())
return ctx.TRUE;
}
return ctx.FALSE;
}
/*----------------------------------------------------------------------------*/
@OpenNTF
@ParamCount(3)
public static ValueHolder atMatchesGetCaptGroups(final FormulaContext ctx, final ValueHolder[] params) {
String tester = params[0].getString(0);
String pattern = params[1].getString(0);
String resName = params[2].getString(0);
Pattern regPatt = Pattern.compile(pattern);
Matcher matsch = regPatt.matcher(tester);
if (!matsch.matches())
return ValueHolder.valueOf(0);
int numGroups = matsch.groupCount();
ValueHolder resValue = ValueHolder.createValueHolder(String.class, numGroups + 1);
for (int i = 0; i <= numGroups; i++)
resValue.add(matsch.group(i));
ctx.setVarLC(resName.toLowerCase(), resValue);
return ValueHolder.valueOf(resValue.size);
}
/*----------------------------------------------------------------------------*/
/*
* @Replace, @ReplaceSubstring
*
* These functions operate a bit differently: E.g.
* @Replace("a":"b"; "a":"b"; "A") --> "A, " whereas
* @ReplaceSubstring("a":"b"; "a":"b"; "A") --> "A, A"
*/
/*----------------------------------------------------------------------------*/
@ParamCount(3)
public static ValueHolder atReplace(final ValueHolder[] params) {
Map<String, String> replacer = getReplaceMap(params[1], params[2]);
ValueHolder vh = params[0];
ValueHolder ret = ValueHolder.createValueHolder(String.class, vh.size);
for (int i = 0; i < vh.size; i++) {
String s = vh.getString(i);
String t = replacer.get(s);
ret.add(t == null ? s : t);
}
return ret;
}
/*----------------------------------------------------------------------------*/
private static Map<String, String> getReplaceMap(final ValueHolder froms, final ValueHolder tos) {
Map<String, String> ret = new HashMap<String, String>();
for (int i = 0; i < froms.size; i++) {
String from = froms.getString(i);
String to = (i >= tos.size) ? "" : tos.getString(i);
if (!ret.containsKey(from))
ret.put(from, to);
}
return ret;
}
/*----------------------------------------------------------------------------*/
@ParamCount(3)
public static ValueHolder atReplaceSubstring(final ValueHolder[] params) {
ValueHolder tests = params[0];
ValueHolder ret = ValueHolder.createValueHolder(String.class, tests.size);
for (int i = 0; i < tests.size; i++)
ret.add(replaceString(tests.getString(i), params[1], params[2]));
return ret;
}
/*----------------------------------------------------------------------------*/
private static String replaceString(String what, final ValueHolder froms, final ValueHolder tos) {
String to = tos.getString(0);
for (int i = 0; i < froms.size; i++) {
if (i != 0 && i < tos.size)
to = tos.getString(i);
what = what.replace(froms.getString(i), to);
}
return what;
}
/*----------------------------------------------------------------------------*/
/*
* @Select, @Subset
*
* Concerning @Select, again, Lotus behaves a bit funny:
* @Select(4) ---> 4 @Select(0) ---> ERROR @Select(-1) ---> ERROR
* @Select(n;1;2;4) ---> 1 for every n<=1 e.g. @Select(-11;1;2;4) ---> 1
*/
/*----------------------------------------------------------------------------*/
@DiffersFromLotus("@Select with only one parameter isn't supported")
@ParamCount({ 2, 99 })
public static ValueHolder atSelect(final ValueHolder[] params) {
int which = params[0].getInt(0);
if (which <= 1)
which = 1;
else if (which >= params.length)
which = params.length - 1;
return params[which];
}
/*----------------------------------------------------------------------------*/
@SuppressWarnings("deprecation")
@ParamCount(2)
public static ValueHolder atSubset(final ValueHolder[] params) {
int count = params[1].getInt(0);
if (count == 0)
throw new IllegalArgumentException("Second argument to @Subset is 0");
int first;
int last;
ValueHolder vh = params[0];
if (count > 0) {
first = 0;
last = (count > vh.size) ? vh.size : count;
} else {
count = -count;
last = vh.size;
first = (count > last) ? 0 : last - count;
}
ValueHolder ret = vh.newInstance(last - first);
/*
* To avoid boxing/unboxing of primitives:
*/
switch (vh.dataType) {
case BOOLEAN:
for (int i = first; i < last; i++)
ret.add(vh.getBoolean(i));
break;
case DOUBLE:
for (int i = first; i < last; i++)
ret.add(vh.getDouble(i));
break;
case INTEGER:
for (int i = first; i < last; i++)
ret.add(vh.getInt(i));
break;
default:
for (int i = first; i < last; i++)
ret.add(vh.getObject(i));
}
return ret;
}
/*----------------------------------------------------------------------------*/
/*
* @IsMember, @IsNotMember, @Member
*/
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static ValueHolder atIsMember(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder whos = params[0];
ValueHolder where = params[1];
for (int i = 0; i < whos.size; i++) {
String who = whos.getString(i);
int j;
for (j = 0; j < where.size; j++)
if (where.getString(j).equals(who))
break;
if (j == where.size)
return ctx.FALSE;
}
return ctx.TRUE;
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static ValueHolder atIsNotMember(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder whos = params[0];
ValueHolder where = params[1];
for (int i = 0; i < whos.size; i++) {
String who = whos.getString(i);
for (int j = 0; j < where.size; j++)
if (where.getString(j).equals(who))
return ctx.FALSE;
}
return ctx.TRUE;
}
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static ValueHolder atMember(final FormulaContext ctx, final ValueHolder[] params) {
String who = params[0].getString(0); // Notes accepts a list of Strings here, but ignores all but the first entry
ValueHolder where = params[1];
int j;
for (j = 0; j < where.size; j++)
if (where.getString(j).equals(who))
break;
if (j == where.size)
j = -1;
return ValueHolder.valueOf(j + 1);
}
/*----------------------------------------------------------------------------*/
/*
* @Unique, @Repeat
*/
/*----------------------------------------------------------------------------*/
@ParamCount({ 1, 1 })
public static ValueHolder atUnique(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder vh = params[0];
ValueHolder ret = ValueHolder.createValueHolder(String.class, vh.size);
for (int i = 0; i < vh.size; i++) {
String s = vh.getString(i);
int j;
for (j = 0; j < ret.size; j++)
if (ret.getString(j).equals(s))
break;
if (j == ret.size)
ret.add(s);
}
return ret;
}
/*----------------------------------------------------------------------------*/
@ParamCount({ 2, 3 })
public static ValueHolder atRepeat(final FormulaContext ctx, final ValueHolder[] params) {
ValueHolder vh = params[0];
int repeator = params[1].getInt(0);
if (repeator < 0)
repeator = -repeator; // Notes behaviour!
int cutter = 65000; // The Notes limit is unclear: The documentation says 1024, but seems to be greater.
if (params.length == 3) {
int i = params[2].getInt(0);
if (i < 0)
i = -i; // Notes behaviour!
if (i < 65000)
cutter = i;
}
int bedarf = 0;
for (int i = 0; i < vh.size; i++) {
int lh = vh.getString(i).length();
if (lh > bedarf)
bedarf = lh;
}
ValueHolder ret = ValueHolder.createValueHolder(String.class, vh.size);
if (repeator == 0 || cutter == 0 || bedarf == 0) {
for (int i = 0; i < vh.size; i++)
ret.add("");
return ret;
}
StringBuilder sb = new StringBuilder(bedarf * repeator);
for (int i = 0; i < vh.size; i++) {
sb.setLength(0);
String s = vh.getString(i);
for (int j = 0; j < repeator; j++)
sb.append(s);
ret.add((sb.length() > cutter) ? sb.substring(0, cutter) : sb.toString());
}
return ret;
}
/*----------------------------------------------------------------------------*/
/*
* @Compare
*/
/*----------------------------------------------------------------------------*/
@DiffersFromLotus({ "Options [ACCENT(IN)SENSITIVE] and [PITCH(IN)SENSITIVE] aren't yet supported",
"String compare is done via String.compareTo" })
@ParamCount({ 2, 3 })
public static ValueHolder atCompare(final ValueHolder[] params) {
boolean caseSensitive = true;
if (params.length == 3) {
ValueHolder options = params[2];
for (int i = 0; i < options.size; i++) {
String opt = options.getString(i);
if ("[CASESENSITIVE]".equalsIgnoreCase(opt))
caseSensitive = true;
else if ("[CASEINSENSITIVE]".equalsIgnoreCase(opt))
caseSensitive = false;
else if (!"[ACCENTSENSITIVE]".equalsIgnoreCase(opt) && !"[ACCENTINSENSITIVE]".equalsIgnoreCase(opt)
&& !"[PITCHSENSITIVE]".equalsIgnoreCase(opt) && !"[PITCHINSENSITIVE]".equalsIgnoreCase(opt))
throw new IllegalArgumentException("Illegal Option: " + opt);
}
}
ValueHolder vh1 = params[0];
ValueHolder vh2 = params[1];
int max = (vh1.size >= vh2.size) ? vh1.size : vh2.size;
ValueHolder ret = ValueHolder.createValueHolder(Integer.class, max);
String s1 = null;
String s2 = null;
for (int i = 0; i < max; i++) {
if (i < vh1.size)
s1 = vh1.getString(i);
if (i < vh2.size)
s2 = vh2.getString(i);
int cmp;
if (s1 == null)
cmp = (s2 == null) ? 0 : -1;
else
cmp = caseSensitive ? s1.compareTo(s2) : s1.compareToIgnoreCase(s2);
if (cmp < 0)
cmp = -1;
else if (cmp > 0)
cmp = 1;
ret.add(cmp);
}
return (ret);
}
/*----------------------------------------------------------------------------*/
/*
* @FileDir
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static String atFileDir(final String fileName) {
int indSlash = fileName.lastIndexOf('/');
int indBackSlash = -1;
if ("\\".equals(System.getProperty("file.separator")))
indBackSlash = fileName.lastIndexOf('\\');
if (indSlash < 0 && indBackSlash < 0)
return "";
if (indSlash < indBackSlash)
indSlash = indBackSlash;
return fileName.substring(0, indSlash + 1);
}
/*----------------------------------------------------------------------------*/
/*
* @Text
*/
/*----------------------------------------------------------------------------*/
@DiffersFromLotus("Not all Lotus formats are yet supported")
@SuppressWarnings("deprecation")
@ParamCount({ 1, 2 })
public static ValueHolder atText(final FormulaContext ctx, final ValueHolder params[]) {
ValueHolder vh = params[0];
String format = null;
if (params.length == 2) {
format = params[1].getString(0).toUpperCase();
if (format.isEmpty() || (!vh.dataType.numeric && vh.dataType != DataType.DATETIME))
format = null;
}
if (vh.dataType.numeric)
return doAtTextNumber(ctx, vh, format);
if (vh.dataType == DataType.DATETIME)
return doAtTextDateTime(ctx, vh, format);
if (vh.dataType == DataType.STRING)
return vh;
ValueHolder ret = ValueHolder.createValueHolder(String.class, vh.size);
for (int i = 0; i < vh.size; i++)
ret.add(vh.get(i).toString());
return ret;
}
/*----------------------------------------------------------------------------*/
private static ValueHolder doAtTextNumber(final FormulaContext ctx, final ValueHolder vh, final String format) {
Formatter.LotusNumberOptions lno = new Formatter.LotusNumberOptions();
if (format == null)
lno.format = 'G';
else {
boolean invalidFormat = false;
int lh = format.length();
for (int i = 0; i < lh; i++) {
char c = format.charAt(i);
if (c == ',') {
lno.useGrouping = true;
continue;
}
if (c == '(' || c == ')') {
lno.negativeAsParentheses = true;
continue;
}
if (c == 'G' || c == 'F' || c == 'C' || c == '%' || c == 'S') {
if (lno.format != 0) {
invalidFormat = true;
break;
}
lno.format = c;
continue;
}
if (!Character.isDigit(c)) {
invalidFormat = true;
break;
}
if (lno.fractionDigits != -1) {
invalidFormat = true;
break;
}
StringBuilder sb = new StringBuilder(64);
sb.append(c);
for (i++; i < lh; i++) {
c = format.charAt(i);
if (!Character.isDigit(c)) {
i--;
break;
}
sb.append(c);
}
try {
lno.fractionDigits = Integer.parseInt(sb.toString());
} catch (NumberFormatException e) {
invalidFormat = true;
break;
}
}
if (invalidFormat)
throw new IllegalArgumentException("Invalid Number format: '" + format + "'");
}
if (lno.format == 0)
lno.format = 'G';
if (lno.format == 'G' || lno.format == '%')
lno.fractionDigits = -1;
else if (lno.format == 'F' && lno.fractionDigits == -1)
lno.fractionDigits = 2;
ValueHolder ret = ValueHolder.createValueHolder(String.class, vh.size);
for (int i = 0; i < vh.size; i++)
ret.add(ctx.getFormatter().formatNumber(vh.getDouble(i), lno));
//ret.add(DominoFormatter.getInstance(DateTimeFunctions.iLocale).formatNumber(vh.getDouble(i), lno));
return ret;
}
/*----------------------------------------------------------------------------*/
private static ValueHolder doAtTextDateTime(final FormulaContext ctx, final ValueHolder vh, final String format) {
Formatter.LotusDateTimeOptions ldto = new Formatter.LotusDateTimeOptions();
boolean invalidFormat = false;
while (format != null) {
int lh = format.length();
if (lh == 0)
break;
invalidFormat = true;
if ((lh & 1) != 0)
break;
int i;
for (i = 0; i < lh; i += 2) {
char opt = format.charAt(i);
char optNum = format.charAt(i + 1);
if (!Character.isDigit(optNum))
break;
if (opt == 'D') {
if (optNum == '0')
ldto.dOption = LotusDateTimeOptions.D_YMD;
else if (optNum == '1')
ldto.dOption = LotusDateTimeOptions.D_YMD_YOPT;
else if (optNum == '2')
ldto.dOption = LotusDateTimeOptions.D_MD;
else if (optNum == '3')
ldto.dOption = LotusDateTimeOptions.D_YM;
else
break;
} else if (opt == 'T') {
if (optNum == '0')
ldto.tOption = LotusDateTimeOptions.T_HMS;
else if (optNum == '1')
ldto.tOption = LotusDateTimeOptions.T_HM;
else
break;
} else if (opt == 'Z') {
if (optNum == '0')
ldto.zOption = LotusDateTimeOptions.Z_CONV;
else if (optNum == '1')
ldto.zOption = LotusDateTimeOptions.Z_DISP_OPT;
else if (optNum == '2')
ldto.zOption = LotusDateTimeOptions.Z_DISP_ALW;
else
break;
} else if (opt == 'S') {
if (optNum == '0')
ldto.sOption = LotusDateTimeOptions.S_D_ONLY;
else if (optNum == '1')
ldto.sOption = LotusDateTimeOptions.S_T_ONLY;
else if (optNum == '2')
ldto.sOption = LotusDateTimeOptions.S_DT;
else if (optNum == '3')
ldto.sOption = LotusDateTimeOptions.S_DT_TY;
else
break;
} else
break;
}
if (i == lh)
invalidFormat = false;
break;
}
if (invalidFormat)
throw new IllegalArgumentException("Invalid DateTime format: '" + format + "'");
ValueHolder ret = ValueHolder.createValueHolder(String.class, vh.size);
for (int i = 0; i < vh.size; i++)
ret.add(ctx.getFormatter().formatDateTime(vh.getDateTime(i), ldto));
return ret;
}
/*----------------------------------------------------------------------------*/
/*
* @TextToDateTimeF, @TextFromDateTimeF
*/
/*----------------------------------------------------------------------------*/
@OpenNTF
@ParamCount({ 2, 3 })
public static ValueHolder atTextToDateTimeF(final FormulaContext ctx, final ValueHolder params[]) {
boolean parseLenient = false;
if (params.length == 3) {
String opt = params[2].getString(0);
if (!"[LENIENT]".equalsIgnoreCase(opt))
throw new IllegalArgumentException("Invalid option: '" + opt + "'");
parseLenient = true;
}
String format = params[1].getString(0);
ValueHolder vh = params[0];
ValueHolder ret = ValueHolder.createValueHolder(DateTime.class, vh.size);
for (int i = 0; i < vh.size; i++)
ret.add(ctx.getFormatter().parseDateWithFormat(vh.getString(i), format, parseLenient));
return ret;
}
/*----------------------------------------------------------------------------*/
@OpenNTF
@ParamCount(2)
public static String atTextFromDateTimeF(final FormulaContext ctx, final DateTime sdt, final String format) {
return ctx.getFormatter().formatDateTimeWithFormat(sdt, format);
}
/*----------------------------------------------------------------------------*/
public static ValueHolder atListSupportedFunctions(final FormulaContext ctx) {
FunctionFactory ff = Formulas.getFunctionFactory();
List<String> functions = new ArrayList<String>(ff.getFunctions().keySet());
Collections.sort(functions);
ValueHolder ret = ValueHolder.createValueHolder(String.class, functions.size());
for (String func : functions) {
ret.add(func);
}
return ret;
}
/*----------------------------------------------------------------------------*/
/*
* @URLDecode, @URLEncode
*/
/*----------------------------------------------------------------------------*/
@ParamCount(2)
public static ValueHolder atURLDecode(final ValueHolder params[]) {
return urlDecEnc(params, false);
}
@ParamCount(2)
public static ValueHolder atURLEncode(final ValueHolder params[]) {
return urlDecEnc(params, true);
}
private static ValueHolder urlDecEnc(final ValueHolder[] params, final boolean encode) {
String opt = params[0].getString(0);
if (!"domino".equalsIgnoreCase(opt) && (!"utf-8".equalsIgnoreCase(opt)))
throw new IllegalArgumentException("Decode type '" + opt + "' not supported");
ValueHolder vh = params[1];
ValueHolder ret = ValueHolder.createValueHolder(String.class, vh.size);
try {
for (int i = 0; i < vh.size; i++) {
String what = vh.getString(i);
ret.add(encode ? URLEncoder.encode(what, "UTF-8").replace("+", "%20") : URLDecoder.decode(what, "UTF-8"));
}
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Error in URLEn/Decoder: " + e.getMessage());
}
return ret;
}
/*----------------------------------------------------------------------------*/
/*
* @CheckFormulaSyntax
*/
/*----------------------------------------------------------------------------*/
@ParamCount(1)
public static ValueHolder atCheckFormulaSyntax(final FormulaContext ctx, final ValueHolder params[]) {
FormulaParser parser = ctx.getParser();
try {
parser.parse(params[0].getString(0));
return ValueHolder.valueOf("1");
} catch (FormulaParseException e) {
return ValueHolder.valueOf(e.getMessage());
}
}
/*----------------------------------------------------------------------------*/
/*
* @GetLocaleDisplayName
*/
/*----------------------------------------------------------------------------*/
@OpenNTF
@ParamCount(1)
public static String atGetLocaleDisplayName(final FormulaContext ctx, final String localeShortName) {
int lh = localeShortName.length();
if (lh < 2)
return localeShortName;
Locale l = getPredefLocale(localeShortName);
if (l == null) {
String language = localeShortName.substring(0, 2);
String country = (lh >= 5) ? localeShortName.substring(3, 5) : "";
l = new Locale(language, country);
}
return l.getDisplayName(ctx.getFormatter().getLocale());
}
public static Locale getPredefLocale(final String localeShortName) {
return lPredefLocs.get(localeShortName);
}
private static Map<String, Locale> lPredefLocs;
static {
lPredefLocs = new HashMap<String, Locale>();
Locale l;
l = Locale.CANADA;
lPredefLocs.put(l.toString(), l);
l = Locale.CANADA_FRENCH;
lPredefLocs.put(l.toString(), l);
l = Locale.CHINA;
lPredefLocs.put(l.toString(), l);
l = Locale.CHINESE;
lPredefLocs.put(l.toString(), l);
l = Locale.ENGLISH;
lPredefLocs.put(l.toString(), l);
l = Locale.FRANCE;
lPredefLocs.put(l.toString(), l);
l = Locale.FRENCH;
lPredefLocs.put(l.toString(), l);
l = Locale.GERMAN;
lPredefLocs.put(l.toString(), l);
l = Locale.GERMANY;
lPredefLocs.put(l.toString(), l);
l = Locale.ITALIAN;
lPredefLocs.put(l.toString(), l);
l = Locale.ITALY;
lPredefLocs.put(l.toString(), l);
l = Locale.JAPAN;
lPredefLocs.put(l.toString(), l);
l = Locale.JAPANESE;
lPredefLocs.put(l.toString(), l);
l = Locale.KOREA;
lPredefLocs.put(l.toString(), l);
l = Locale.KOREAN;
lPredefLocs.put(l.toString(), l);
l = Locale.PRC;
lPredefLocs.put(l.toString(), l);
l = Locale.SIMPLIFIED_CHINESE;
lPredefLocs.put(l.toString(), l);
l = Locale.TAIWAN;
lPredefLocs.put(l.toString(), l);
l = Locale.TRADITIONAL_CHINESE;
lPredefLocs.put(l.toString(), l);
l = Locale.UK;
lPredefLocs.put(l.toString(), l);
l = Locale.US;
lPredefLocs.put(l.toString(), l);
}
/*----------------------------------------------------------------------------*/
}