From 91fff62fd5f94991f00131100bba1ff3ead6f1ad Mon Sep 17 00:00:00 2001 From: Robbendebiene Date: Tue, 29 Aug 2023 16:19:22 +0200 Subject: [PATCH] Make expressions return 0 to n Strings --- lib/models/expression_handler.dart | 143 +++++++++++------- lib/models/map_feature.dart | 2 +- .../question_catalog/answer_constructor.dart | 9 +- test/answer_constructor_test.dart | 25 +++ 4 files changed, 120 insertions(+), 59 deletions(-) diff --git a/lib/models/expression_handler.dart b/lib/models/expression_handler.dart index 1bf635f9..0ab7861a 100644 --- a/lib/models/expression_handler.dart +++ b/lib/models/expression_handler.dart @@ -6,7 +6,7 @@ typedef SubstitutionCallback = Iterable Function(String variableName); /// Expressions must not throw an error. /// Instead they return a meaningful result if possible or null. -typedef ExpressionCallback = String? Function(Iterable); +typedef ExpressionCallback = Iterable Function(Iterable); /// A utility class that can be mixed in to get expression support wherever needed. @@ -16,7 +16,7 @@ typedef ExpressionCallback = String? Function(Iterable); /// 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: /// ``` @@ -45,7 +45,7 @@ mixin ExpressionHandler { /// Substitutes any variables (marked by $) and then executes the given expression array. - String? evaluateExpression(Iterable rawExpression, SubstitutionCallback substitutionCallback) { + Iterable evaluateExpression(Iterable rawExpression, SubstitutionCallback substitutionCallback) { if (rawExpression.isEmpty) { throw const InvalidExpression('An expression list must not be empty.'); } @@ -87,10 +87,7 @@ 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); } } } @@ -98,59 +95,87 @@ mixin ExpressionHandler { // expression functions -String? _join(Iterable args) { - if (args.isEmpty) return null; +Iterable _join(Iterable 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 args) { - return args.isEmpty ? null : args.join(); +Iterable _concat(Iterable 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 args) { - return args.isEmpty ? null : args.first; +Iterable _coalesce(Iterable 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 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 _couple(Iterable 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 args) { +Iterable _pad(Iterable 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. @@ -159,38 +184,42 @@ String? _pad(Iterable 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 args) { +Iterable _insert(Iterable 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 args) { +Iterable _replace(Iterable args) sync* { final iter = args.iterator; - if (!iter.moveNext()) { - return null; - } + + if (!iter.moveNext()) return; final Pattern pattern; // parse RegExp from String @@ -199,16 +228,20 @@ String? _replace(Iterable 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; diff --git a/lib/models/map_feature.dart b/lib/models/map_feature.dart index cc0e454a..f42a5bd3 100644 --- a/lib/models/map_feature.dart +++ b/lib/models/map_feature.dart @@ -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'); } diff --git a/lib/models/question_catalog/answer_constructor.dart b/lib/models/question_catalog/answer_constructor.dart index 7f52c934..0f8301bd 100644 --- a/lib/models/question_catalog/answer_constructor.dart +++ b/lib/models/question_catalog/answer_constructor.dart @@ -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; diff --git a/test/answer_constructor_test.dart b/test/answer_constructor_test.dart index 1c7d5ef8..a7e5f298 100644 --- a/test/answer_constructor_test.dart +++ b/test/answer_constructor_test.dart @@ -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', + }), + ); + } }); }