Skip to content

Commit

Permalink
Make expressions return 0 to n Strings
Browse files Browse the repository at this point in the history
  • Loading branch information
Robbendebiene committed Aug 29, 2023
1 parent f3ac708 commit 91fff62
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 59 deletions.
143 changes: 88 additions & 55 deletions lib/models/expression_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ typedef SubstitutionCallback = Iterable<String> Function(String variableName);
/// Expressions must not throw an error.
/// Instead they return a meaningful result if possible or null.
typedef ExpressionCallback = String? Function(Iterable<String>);
typedef ExpressionCallback = Iterable<String> Function(Iterable<String>);


/// A utility class that can be mixed in to get expression support wherever needed.
Expand All @@ -16,7 +16,7 @@ typedef ExpressionCallback = String? Function(Iterable<String>);
/// as arguments/parameters to the specific expression function.
///
/// Expressions can also contain variables which are marked with a `$` symbol at the start.
/// Variables may resolve into multiple values, but an expression has exactly one result.
/// Variables and expressions may resolve into multiple values.
///
/// Example:
/// ```
Expand Down Expand Up @@ -45,7 +45,7 @@ mixin ExpressionHandler {

/// Substitutes any variables (marked by $) and then executes the given expression array.
String? evaluateExpression(Iterable<dynamic> rawExpression, SubstitutionCallback substitutionCallback) {
Iterable<String> evaluateExpression(Iterable<dynamic> rawExpression, SubstitutionCallback substitutionCallback) {
if (rawExpression.isEmpty) {
throw const InvalidExpression('An expression list must not be empty.');
}
Expand Down Expand Up @@ -87,70 +87,95 @@ mixin ExpressionHandler {
}
else if (arg is Iterable) {
// evaluate nested expressions recursively
final result = evaluateExpression(arg, substitutionCallback);
if (result != null) {
yield result;
}
yield* evaluateExpression(arg, substitutionCallback);
}
}
}
}

// expression functions

String? _join(Iterable<String> args) {
if (args.isEmpty) return null;
Iterable<String> _join(Iterable<String> args) sync* {
final iter = args.iterator;

if (!iter.moveNext()) return;
final delimiter = iter.current;

final delimiter = args.first;
final values = args.skip(1);
return values.isEmpty ? null : values.join(delimiter);
if (!iter.moveNext()) return;
final buffer = StringBuffer(iter.current);

while (iter.moveNext()) {
buffer
..write(delimiter)
..write(iter.current);
}
yield buffer.toString();
}

/// Returns the concatenation of all inputs.
String? _concat(Iterable<String> args) {
return args.isEmpty ? null : args.join();
Iterable<String> _concat(Iterable<String> args) sync* {
final iter = args.iterator;

if (!iter.moveNext()) return;
final buffer = StringBuffer(iter.current);

while (iter.moveNext()) {
buffer.write(iter.current);
}
yield buffer.toString();
}

/// Returns the first input and discards the others.
String? _coalesce(Iterable<String> args) {
return args.isEmpty ? null : args.first;
Iterable<String> _coalesce(Iterable<String> args) sync* {
final iter = args.iterator;
if (iter.moveNext()) yield iter.current;
}

/// Concatenates exactly two values. If less or more values are given this returns null.
/// Concatenates exactly two values. If less or more values are given this returns empty.
String? _couple(Iterable<String> args) {
// manually iterate and count, since using length property is more expensive on the given iterable
var i = 0;
final buffer = StringBuffer();
for (final arg in args) {
buffer.write(arg);
i++;
}
return (i != 2) ? null : buffer.toString();
Iterable<String> _couple(Iterable<String> args) sync* {
final iter = args.iterator;

if (!iter.moveNext()) return;
final firstString = iter.current;

if (!iter.moveNext()) return;
final secondString = iter.current;

// empty return when more then 2 values are provided
if (iter.moveNext()) return;

yield firstString + secondString;
}

/// Adds a given String to a target String for each time the target String length is less than a given width.
/// First arg is the padding String.
/// Second arg is the desired width. Positive values will prepend, negative values will append to the target String.
/// Third arg is the target String.
/// Any following args are the target Strings.
String? _pad(Iterable<String> args) {
Iterable<String> _pad(Iterable<String> args) sync* {
final iter = args.iterator;
if (!iter.moveNext()) return null;

if (!iter.moveNext()) return;
final paddingString = iter.current;
if (!iter.moveNext()) return null;

if (!iter.moveNext()) return;
final width = int.tryParse(iter.current);
if (!iter.moveNext() || width == null) return null;

final mainString = iter.current;
if (width == null) return;

return width.isNegative
? mainString.padRight(width.abs(), paddingString)
: mainString.padLeft(width, paddingString);
if (width.isNegative) {
while (iter.moveNext()) {
yield iter.current.padRight(width.abs(), paddingString);
}
}
else {
while (iter.moveNext()) {
yield iter.current.padLeft(width, paddingString);
}
}
}

/// Inserts a given String into a target String.
Expand All @@ -159,38 +184,42 @@ String? _pad(Iterable<String> args) {
/// Negative positions are treated as insertions starting at the end of the String.
/// So -1 means insert before the last character of the target String.
/// If the index exceeds the length of the target String, it will be returned without any modifications.
/// Third arg is the target String.
/// Any following args are the target Strings.
String? _insert(Iterable<String> args) {
Iterable<String> _insert(Iterable<String> args) sync* {
final iter = args.iterator;
if (!iter.moveNext()) return null;

if (!iter.moveNext()) return;
final insertionString = iter.current;
if (!iter.moveNext()) return null;

if (!iter.moveNext()) return;
final position = int.tryParse(iter.current);
if (!iter.moveNext() || position == null) return null;

final mainString = iter.current;
if (mainString.length < position.abs()) return mainString;

final index = position.isNegative
? mainString.length + position
: position;
if (position == null) return;

return mainString.replaceRange(index, index, insertionString);
while (iter.moveNext()) {
final mainString = iter.current;
if (mainString.length < position.abs()) {
yield mainString;
}
else {
final index = position.isNegative
? mainString.length + position
: position;
yield mainString.replaceRange(index, index, insertionString);
}
}
}

/// Replaces a given Pattern (either String or RegExp) in a target String by a given replacement String.
/// First arg is the Pattern the target String should be matched against.
/// Second arg is the replacement String.
/// Third arg is the target String.
/// Any following args are the target Strings.
String? _replace(Iterable<String> args) {
Iterable<String> _replace(Iterable<String> args) sync* {
final iter = args.iterator;
if (!iter.moveNext()) {
return null;
}

if (!iter.moveNext()) return;
final Pattern pattern;

// parse RegExp from String
Expand All @@ -199,16 +228,20 @@ String? _replace(Iterable<String> args) {
pattern = RegExp(iter.current.substring(1, iter.current.length - 1));
}
on FormatException {
return null;
return;
}
}
else {
pattern = iter.current;
}
if (!iter.moveNext()) return null;

if (!iter.moveNext()) return;
final replacementString = iter.current;
if (!iter.moveNext()) return null;

while (iter.moveNext()) {
yield iter.current.replaceAll(pattern, replacementString);
}
}

final mainString = iter.current;

Expand Down
2 changes: 1 addition & 1 deletion lib/models/map_feature.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class MapFeature with ExpressionHandler {
}
});
// replace \n with new line character
return result?.replaceAll(r'\n' ,'\n');
return result.firstOrNull?.replaceAll(r'\n' ,'\n');
}


Expand Down
9 changes: 6 additions & 3 deletions lib/models/question_catalog/answer_constructor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ class AnswerConstructor with ExpressionHandler {
yield* values;
}
});
// write osm tag if the expression did not return null
if (result != null) {
map[entry.key] = result;
// write first value of expression to osm tag
try {
map[entry.key] = result.first;
}
on StateError {
// no osm tag will be written
}
}
return map;
Expand Down
25 changes: 25 additions & 0 deletions test/answer_constructor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -754,5 +754,30 @@ void main() async {
})
);
}

{
const testConstructor = AnswerConstructor({
'key1': [
'CONCAT', [
'INSERT', '_', '-1', [
'PAD', '#', '4', r'$input',
],
],
],
'key2': [
'JOIN', '.', [
'REPLACE', 'a', 'x', r'$input'
],
],
});

expect(
testConstructor.construct(withValues),
equals({
'key1': '###_a###_b###_c',
'key2': 'x.b.c',
}),
);
}
});
}

0 comments on commit 91fff62

Please sign in to comment.