Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more expressions #179

Merged
merged 9 commits into from
Jun 29, 2023
Merged
2 changes: 1 addition & 1 deletion assets/datasets/expression_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"anyOf": [
{
"type": "string",
"enum": ["COALESCE", "JOIN", "CONCAT", "COUPLE"]
"enum": ["COALESCE", "JOIN", "CONCAT", "COUPLE", "PAD", "INSERT", "REPLACE"]
},
{
"type": "string",
Expand Down
56 changes: 56 additions & 0 deletions docs/QUESTION_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,62 @@ output: `operator=A_suffix`
constructor: `"operator": ["COUPLE", "$input", "_suffix"]`
output: `operator=NULL`

#### `INSERT` expression

Inserts one String into another String at a certain position.

First argument represents the insertion String.
Second argument specifies the position/index where the String should be inserted into the target String. 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 argument resembles the target String.

**Examples:**
- input: `[tag_value]`
constructor: `"operator": ["INSERT", "X", "1", "$input"]`
output: `operator=tXag_value`
- input: `[tag_value]`
constructor: `"operator": ["INSERT", "X", "-5", "$input"]`
output: `operator=tag_Xvalue`
- input: `[tag_value]`
constructor: `"operator": ["INSERT", "X", "20", "$input"]`
output: `operator=tag_value`

#### `PAD` expression

Adds a given String to a target String for each time the target String length is less than a given width.

First argument represents the padding String.
Second argument specifies the desired width. Positive values will prepend, negative values will append to the target String.
Third argument resembles the target String.

**Examples:**
- input: `[1]`
constructor: `"operator": ["PAD", "0", "3", "$input"]`
output: `operator=001`
- input: `[1]`
constructor: `"operator": ["PAD", "0", "-3", "$input"]`
output: `operator=100`
- input: `[value]`
constructor: `"operator": ["PAD", "XXX", "9", "$input"]`
output: `operator=XXXXXXvalue`

#### `REPLACE` expression

Replaces a given Pattern (either String or RegExp) in a target String by a given replacement String.
RegExp are denoted by a `/` at the start and end of the String.
First argument represents the Pattern the target String should be matched against.
Second argument defines the replacement String.
Third argument resembles the target String.

**Examples:**
- input: `[sometimes]`
constructor: `"operator": ["REPLACE", "times", "thing", "$input"]`
output: `operator=something`
- input: `[sometimes]`
constructor: `"operator": ["REPLACE", "e", "#", "$input"]`
output: `operator=som#tim#s`
- input: `[value]`
constructor: `"operator": ["REPLACE", "/^.|.$/", "_", "$input"]`
output: `operator=_alu_`

### Answer examples

Expand Down
102 changes: 96 additions & 6 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 Expression = String? Function(Iterable<String>);
typedef ExpressionCallback = String? Function(Iterable<String>);


/// A utility class that can be mixed in to get expression support wherever needed.
Expand All @@ -33,11 +33,14 @@ mixin ExpressionHandler {

/// A name to function mapping for expressions.

static const _expressionMapping = <String, Expression>{
static const _expressionMapping = <String, ExpressionCallback>{
'JOIN': _join,
'CONCAT': _concat,
'COALESCE': _coalesce,
'COUPLE': _couple,
'PAD': _pad,
'INSERT': _insert,
'REPLACE': _replace,
};

/// Substitutes any variables (marked by $) and then executes the given expression array.
Expand All @@ -48,7 +51,7 @@ mixin ExpressionHandler {
}

final expressionIdentifier = rawExpression.first;
final Expression? expression;
final ExpressionCallback? expression;
final Iterable parameters;

if (expressionIdentifier is! String || expressionIdentifier.isEmpty) {
Expand Down Expand Up @@ -96,18 +99,21 @@ mixin ExpressionHandler {
// expression functions

String? _join(Iterable<String> args) {
if (args.isEmpty) {
return null;
}
if (args.isEmpty) return null;

final delimiter = args.first;
final values = args.skip(1);
return values.isEmpty ? null : values.join(delimiter);
}

/// Returns the concatenation of all inputs.

String? _concat(Iterable<String> args) {
return args.isEmpty ? null : args.join();
}

/// Returns the first input and discards the others.

String? _coalesce(Iterable<String> args) {
return args.isEmpty ? null : args.first;
}
Expand All @@ -125,6 +131,90 @@ String? _couple(Iterable<String> args) {
return (i != 2) ? null : buffer.toString();
}

/// 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.

String? _pad(Iterable<String> args) {
final iter = args.iterator;
if (!iter.moveNext()) return null;

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

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

final mainString = iter.current;

return width.isNegative
? mainString.padRight(width.abs(), paddingString)
: mainString.padLeft(width, paddingString);
}

/// Inserts a given String into a target String.
/// First arg is the insertion String.
/// Second arg is the position/index where the String should be inserted into the target String.
/// 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.

String? _insert(Iterable<String> args) {
final iter = args.iterator;
if (!iter.moveNext()) return null;

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

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;

return 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.

String? _replace(Iterable<String> args) {
final iter = args.iterator;
if (!iter.moveNext()) {
return null;
}
final Pattern pattern;

// parse RegExp from String
if (iter.current.startsWith('/') && iter.current.endsWith('/')) {
try {
pattern = RegExp(iter.current.substring(1, iter.current.length - 1));
}
on FormatException {
return null;
}
}
else {
pattern = iter.current;
}
if (!iter.moveNext()) return null;

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

final mainString = iter.current;

return mainString.replaceAll(pattern, replacementString);
}

/// Indicates that an expression is malformed.

class InvalidExpression implements Exception {
Expand Down
Loading
Loading