feat(functions): add text functions

This commit is contained in:
dualfroz committed 2026-08-25 18:07:00 +02:00
1 parent 7c2df12a72
commit 6442c8f575
1 file changed
+230
+230
View File
@@ -0,0 +1,230 @@
import 'dart:math' as math;
import '../evaluator.dart';
import '../format.dart';
import '../values.dart';
import 'helpers.dart';
void registerText(Map<String, CellFunction> registry) {
registry['CONCAT'] = (args, ctx) {
final buffer = StringBuffer();
for (final value in ctx.flatten(args)) {
buffer.write(ctx.asText(value));
}
return TextValue(buffer.toString());
};
registry['CONCATENATE'] = registry['CONCAT']!;
registry['TEXTJOIN'] = (args, ctx) {
arity(args, 3);
final delimiter = textArg(args[0], ctx);
final ignoreEmpty = boolArg(args[1], ctx);
final parts = <String>[];
for (final value in ctx.flatten(args.sublist(2))) {
final text = ctx.asText(value);
if (ignoreEmpty && text.isEmpty) continue;
parts.add(text);
}
return TextValue(parts.join(delimiter));
};
registry['LEN'] = (args, ctx) {
arity(args, 1, 1);
return NumberValue(textArg(args[0], ctx).length.toDouble());
};
registry['LEFT'] = (args, ctx) {
arity(args, 1, 2);
final text = textArg(args[0], ctx);
final count = args.length > 1 ? intArg(args[1], ctx) : 1;
if (count < 0) throw const EvalException(ErrorValue(ErrorKind.value));
return TextValue(text.substring(0, math.min(count, text.length)));
};
registry['RIGHT'] = (args, ctx) {
arity(args, 1, 2);
final text = textArg(args[0], ctx);
final count = args.length > 1 ? intArg(args[1], ctx) : 1;
if (count < 0) throw const EvalException(ErrorValue(ErrorKind.value));
final take = math.min(count, text.length);
return TextValue(text.substring(text.length - take));
};
registry['MID'] = (args, ctx) {
arity(args, 3, 3);
final text = textArg(args[0], ctx);
final start = intArg(args[1], ctx);
final length = intArg(args[2], ctx);
if (start < 1 || length < 0) throw const EvalException(ErrorValue(ErrorKind.value));
if (start > text.length) return const TextValue('');
final begin = start - 1;
final end = math.min(begin + length, text.length);
return TextValue(text.substring(begin, end));
};
registry['UPPER'] = (args, ctx) {
arity(args, 1, 1);
return TextValue(textArg(args[0], ctx).toUpperCase());
};
registry['LOWER'] = (args, ctx) {
arity(args, 1, 1);
return TextValue(textArg(args[0], ctx).toLowerCase());
};
registry['PROPER'] = (args, ctx) {
arity(args, 1, 1);
final text = textArg(args[0], ctx);
final buffer = StringBuffer();
var atWordStart = true;
for (final rune in text.runes) {
final ch = String.fromCharCode(rune);
final isLetter = RegExp(r'[A-Za-z]').hasMatch(ch);
if (isLetter) {
buffer.write(atWordStart ? ch.toUpperCase() : ch.toLowerCase());
atWordStart = false;
} else {
buffer.write(ch);
atWordStart = true;
}
}
return TextValue(buffer.toString());
};
registry['TRIM'] = (args, ctx) {
arity(args, 1, 1);
return TextValue(textArg(args[0], ctx).trim().replaceAll(RegExp(r'\s+'), ' '));
};
registry['REPT'] = (args, ctx) {
arity(args, 2, 2);
final count = intArg(args[1], ctx);
if (count < 0) throw const EvalException(ErrorValue(ErrorKind.value));
return TextValue(textArg(args[0], ctx) * count);
};
registry['EXACT'] = (args, ctx) {
arity(args, 2, 2);
return BooleanValue(textArg(args[0], ctx) == textArg(args[1], ctx));
};
registry['SUBSTITUTE'] = (args, ctx) {
arity(args, 3, 4);
final text = textArg(args[0], ctx);
final oldText = textArg(args[1], ctx);
final newText = textArg(args[2], ctx);
if (oldText.isEmpty) return TextValue(text);
if (args.length == 3) return TextValue(text.replaceAll(oldText, newText));
final instance = intArg(args[3], ctx);
if (instance < 1) throw const EvalException(ErrorValue(ErrorKind.value));
return TextValue(_replaceNth(text, oldText, newText, instance));
};
registry['REPLACE'] = (args, ctx) {
arity(args, 4, 4);
final text = textArg(args[0], ctx);
final start = intArg(args[1], ctx);
final length = intArg(args[2], ctx);
final replacement = textArg(args[3], ctx);
if (start < 1 || length < 0) throw const EvalException(ErrorValue(ErrorKind.value));
final begin = math.min(start - 1, text.length);
final end = math.min(begin + length, text.length);
return TextValue(text.substring(0, begin) + replacement + text.substring(end));
};
registry['FIND'] = (args, ctx) {
arity(args, 2, 3);
final needle = textArg(args[0], ctx);
final haystack = textArg(args[1], ctx);
final start = args.length > 2 ? intArg(args[2], ctx) : 1;
if (start < 1) throw const EvalException(ErrorValue(ErrorKind.value));
final index = haystack.indexOf(needle, start - 1);
if (index < 0) throw const EvalException(ErrorValue(ErrorKind.value));
return NumberValue((index + 1).toDouble());
};
registry['SEARCH'] = (args, ctx) {
arity(args, 2, 3);
final needle = textArg(args[0], ctx).toLowerCase();
final haystack = textArg(args[1], ctx).toLowerCase();
final start = args.length > 2 ? intArg(args[2], ctx) : 1;
if (start < 1) throw const EvalException(ErrorValue(ErrorKind.value));
final index = haystack.indexOf(needle, start - 1);
if (index < 0) throw const EvalException(ErrorValue(ErrorKind.value));
return NumberValue((index + 1).toDouble());
};
registry['CHAR'] = (args, ctx) {
arity(args, 1, 1);
final code = intArg(args[0], ctx);
if (code < 1 || code > 0x10FFFF) throw const EvalException(ErrorValue(ErrorKind.value));
return TextValue(String.fromCharCode(code));
};
registry['CODE'] = (args, ctx) {
arity(args, 1, 1);
final text = textArg(args[0], ctx);
if (text.isEmpty) throw const EvalException(ErrorValue(ErrorKind.value));
return NumberValue(text.runes.first.toDouble());
};
registry['VALUE'] = (args, ctx) {
arity(args, 1, 1);
final parsed = double.tryParse(textArg(args[0], ctx).trim());
if (parsed == null) throw const EvalException(ErrorValue(ErrorKind.value));
return NumberValue(parsed);
};
registry['NUMBERVALUE'] = (args, ctx) {
arity(args, 1, 3);
var text = textArg(args[0], ctx).trim();
final decimalSep = args.length > 1 ? textArg(args[1], ctx) : '.';
final groupSep = args.length > 2 ? textArg(args[2], ctx) : ',';
text = text.replaceAll(groupSep, '');
if (decimalSep != '.') text = text.replaceAll(decimalSep, '.');
final parsed = double.tryParse(text);
if (parsed == null) throw const EvalException(ErrorValue(ErrorKind.value));
return NumberValue(parsed);
};
registry['TEXT'] = (args, ctx) {
arity(args, 2, 2);
final value = ctx.eval(args[0]);
final code = textArg(args[1], ctx);
return TextValue(NumberFormat.fromCode(code).apply(value));
};
registry['REGEXMATCH'] = (args, ctx) {
arity(args, 2, 2);
final pattern = RegExp(textArg(args[1], ctx));
return BooleanValue(pattern.hasMatch(textArg(args[0], ctx)));
};
registry['REGEXEXTRACT'] = (args, ctx) {
arity(args, 2, 2);
final pattern = RegExp(textArg(args[1], ctx));
final match = pattern.firstMatch(textArg(args[0], ctx));
if (match == null) throw const EvalException(ErrorValue(ErrorKind.na));
return TextValue(match.groupCount >= 1 ? (match.group(1) ?? '') : match.group(0)!);
};
registry['REGEXREPLACE'] = (args, ctx) {
arity(args, 3, 3);
final pattern = RegExp(textArg(args[1], ctx));
return TextValue(textArg(args[0], ctx).replaceAll(pattern, textArg(args[2], ctx)));
};
}
String _replaceNth(String text, String oldText, String newText, int instance) {
var searchFrom = 0;
var seen = 0;
while (true) {
final index = text.indexOf(oldText, searchFrom);
if (index < 0) return text;
seen++;
if (seen == instance) {
return text.substring(0, index) + newText + text.substring(index + oldText.length);
}
searchFrom = index + oldText.length;
}
}