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

Advance duration answer #201

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions assets/question_catalog/definition.json
Original file line number Diff line number Diff line change
Expand Up @@ -2646,14 +2646,24 @@
"answer": {
"type": "Duration",
"input": {
"max": 3,
"unit_step": {
"minutes": 1,
"seconds": 1
"max": 2,
"hours": {
"display": false,
"return": true
},
"minutes": {
"step": 1
},
"seconds": {
"step": 1
}
},
"constructor": {
"duration": ["$input"]
"duration": [
"JOIN", ":", [
"PAD", "0", "2", "$input"
]
]
}
},
"conditions": [
Expand Down
73 changes: 58 additions & 15 deletions docs/QUESTION_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,25 +95,38 @@ Displays a small text input to the user which allows entering numbers only.

#### `Duration` input

Displays a number wheel for each specified time unit. Possible time units are `seconds`, `minutes`, `hours` and `days`.
Displays a number wheel for each specified time unit. Possible time units are `days`, `hours`, `minutes` and `seconds`.

The duration is always completely returned to the constructor, meaning no duration inputs are lost.
If for example the input is in hours, minutes and seconds, but only hours is marked as a return value, the hours value will include the minutes and seconds in its representation (potentially in the fractional part). Make sure that the `constructor` only generates the permitted values of the corresponding tag.

The `$input` variable will contain all duration values marked with `return: true` in the following order: `days`, `hours`, `minutes` and `seconds`. Therefore at least one duration value should have `return": true`.

```jsonc
"answer": {
"type": "Duration",
"input": {
// Maximum allowed value for the biggest time unit.
// Maximum allowed input value for the biggest time unit with `display: true`.
"max": 3,
// Defines which time units are available and their step size.
"unit_step": {
// Defines the usage of minutes
"minutes": {
// The time segment/step size of the minutes number wheel.
"minutes": 1,
// The time segment/step size of the seconds number wheel.
"seconds": 1,
"step": 1,
// Whether a separate minutes input should be shown to the user.
// Defaults to true when the unit is defined otherwise false.
"display": true,
// Defines whether minutes should be returned as a separate value in the answer constructor.
// Defaults to true when the unit is defined otherwise false.
"return": true,
},
// Defines the usage of seconds
"seconds": {
...
}
},
...
},
// Mandatory since the tags/keys cannot be derived.
// $input will contain the entered duration in the hh:mm:ss format.
// The duration will be split into a separate value for each unit with "return" set to true.
"constructor": { }
},
```
Expand Down Expand Up @@ -261,11 +274,12 @@ output: `operator=NULL`

#### `INSERT` expression

Inserts one String into another String at a certain position.
Inserts one String into one or multiple other Strings at a certain position.
*This expression can have multiple return values.*

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.
All succeeding arguments resemble the target Strings. For each target string a respective result value will be returned.

**Examples:**
- input: `[tag_value]`
Expand All @@ -280,11 +294,12 @@ 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.
Adds a given String to one or multiple other Strings for each time the target String length is less than the given width.
*This expression can have multiple return values.*

First argument represents the padding String.
Second argument specifies the desired width. Positive values will prepend, negative values will append to the target String. Remember that the final String length may be greater than the desired width when the padding String contains more than one character.
Third argument resembles the target String.
All succeeding arguments resemble the target Strings. For each target string a respective result value will be returned.

**Examples:**
- input: `[1]`
Expand All @@ -299,11 +314,13 @@ output: `operator=XXXXXXvalue`

#### `REPLACE` expression

Replaces a given Pattern (either String or RegExp) in a target String by a given replacement String.
Replaces a given Pattern (either String or RegExp) in one or multiple target Strings with a given replacement String.
*This expression can have multiple return values.*

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.
All succeeding arguments resemble the target Strings. For each target string a respective result value will be returned.

**Examples:**
- input: `[sometimes]`
Expand Down Expand Up @@ -428,6 +445,32 @@ The example makes use of the 3 expressions PAD, INSERT and REPLACE to convert fr
}
```

#### Using expressions to write duration value according to ISO 8601 (HH:MM)
The example will write the entered value in minutes to the *duration* tag in the format HH:MM, e.g. 72 minutes will become 01:12.
**Explanation:** The `$input` variable will contain all values with `return` set to `true`. Single digit values will be padded with a leading zero and finally concatenated by the `JOIN` expression.

```jsonc
"answer": {
"type": "Duration",
"input": {
"max": 90,
"hours": {
"display": false
},
"minutes": {
"step": 1
}
},
"constructor": {
"duration": [
"JOIN", ":", [
"PAD", "0", "2", "$input"
]
]
}
}
```


## The `conditions` part

Expand Down
128 changes: 78 additions & 50 deletions lib/models/answer.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';

import 'question_catalog/answer_definition.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Expand All @@ -18,19 +21,19 @@ abstract class Answer<D extends AnswerDefinition, T> {
final T value;

/// The OSM tag to variable mapping.
/// This defines the values per tag used in the constructor for creating the final OSM tags.
/// This resolves the values for a given tag used in the constructor for creating the final OSM tags.
///
/// The following example defines two values for the `operator` tag:
/// `{ 'operator': [ 'value1', 'value2' ] }`
///
/// Every answer needs to implement this.

Map<String, Iterable<String>> get _variables;
Iterable<String> _resolve(String key);

/// Build OSM tags based on the given value.

Map<String, String> toTagMap() {
return definition.constructor.construct(_variables);
return definition.constructor.construct(_resolve);
}

/// Whether the value of this answer is valid or not.
Expand All @@ -48,12 +51,8 @@ class StringAnswer extends Answer<StringAnswerDefinition, String> {
});

@override
Map<String, Iterable<String>> get _variables {
// assign every tag used in the constructor the input value of this answer
final keys = definition.constructor.tagConstructorDef.keys;
return <String, Iterable<String>>{
for (final key in keys) key : [ value ]
};
Iterable<String> _resolve(String key) sync* {
yield value;
}

@override
Expand All @@ -77,13 +76,9 @@ class NumberAnswer extends Answer<NumberAnswerDefinition, String> {
});

@override
Map<String, Iterable<String>> get _variables {
// assign every tag used in the constructor the input value of this answer
final keys = definition.constructor.tagConstructorDef.keys;
return <String, Iterable<String>>{
// replace decimal comma with period
for (final key in keys) key : [ value.replaceAll(',', '.') ]
};
Iterable<String> _resolve(String key) sync* {
// replace decimal comma with period
yield value.replaceAll(',', '.');
}

@override
Expand Down Expand Up @@ -134,12 +129,11 @@ class BoolAnswer extends Answer<BoolAnswerDefinition, bool> {
});

@override
Map<String, Iterable<String>> get _variables {
// transform all tags of the selected answer to the variable mapping structure
final tags = definition.input[_valueIndex].osmTags.entries;
return <String, Iterable<String>>{
for (final tag in tags) tag.key : [ tag.value ]
};
Iterable<String> _resolve(String key) sync* {
// return matching tag value of the selected answer if any
final tags = definition.input[_valueIndex].osmTags;
final tagValue = tags[key];
if (tagValue != null) yield tagValue;
}

@override
Expand All @@ -161,12 +155,11 @@ class ListAnswer extends Answer<ListAnswerDefinition, int> {
});

@override
Map<String, Iterable<String>> get _variables {
// transform all tags of the selected answer to the variable mapping structure
final tags = definition.input[value].osmTags.entries;
return <String, Iterable<String>>{
for (final tag in tags) tag.key : [ tag.value ]
};
Iterable<String> _resolve(String key) sync* {
// return matching tag value of the selected answer if any
final tags = definition.input[value].osmTags;
final tagValue = tags[key];
if (tagValue != null) yield tagValue;
}

@override
Expand All @@ -187,18 +180,12 @@ class MultiListAnswer extends Answer<ListAnswerDefinition, List<int>> {
});

@override
Map<String, Iterable<String>> get _variables {
// combine all tags of the selected answers
final map = <String, List<String>>{};
Iterable<String> _resolve(String key) sync* {
for (final index in value) {
for (final tag in definition.input[index].osmTags.entries) {
map.update(tag.key,
(value) => value..add(tag.value),
ifAbsent: () => [ tag.value ],
);
}
final tags = definition.input[index].osmTags;
final tagValue = tags[key];
if (tagValue != null) yield tagValue;
}
return map;
}

@override
Expand All @@ -219,19 +206,60 @@ class DurationAnswer extends Answer<DurationAnswerDefinition, Duration> {
});

@override
Map<String, Iterable<String>> get _variables {
// assign every tag used in the constructor the input value of this answer
final keys = definition.constructor.tagConstructorDef.keys;
return <String, Iterable<String>>{
for (final key in keys) key : [ _valueAsHMS() ]
};
}
Iterable<String> _resolve(String key) => _values.map((v){
final formatter = NumberFormat()
..minimumFractionDigits = 0
..maximumFractionDigits = 3;
return formatter.format(v);
});

/// Returns days, hours, minutes and seconds in the specified order.
///
/// Whether the duration part is returned depends on the DurationInputDefinition.

Iterable<num> get _values sync* {
var (fractionalDays, fractionalHours, fractionalMinutes) = (0.0, 0.0, 0.0);

// calculate remainder as decimal if any (result will be 0 if none is required)
// required to handle cases where input and output differs
// e.g. user is able to input hours, minutes, seconds, but the output is only in hours

if (definition.input.seconds.output) {
// noop
}
else if (definition.input.minutes.output) {
fractionalMinutes = (value.inSeconds % Duration.secondsPerMinute) / Duration.secondsPerMinute;
}
else if (definition.input.hours.output) {
fractionalHours = (value.inSeconds % Duration.secondsPerHour) / Duration.secondsPerHour;
}
else if (definition.input.days.output) {
fractionalDays = (value.inSeconds % Duration.secondsPerDay) / Duration.secondsPerDay;
}

// wrap duration parts according to the units present in the output

String _valueAsHMS() {
final hours = value.inHours.toString().padLeft(2, '0');
final minutes = (value.inMinutes % 60).toString().padLeft(2, '0');
final seconds = (value.inSeconds % 60).toString().padLeft(2, '0');
return '$hours:$minutes:$seconds';
const maxInteger = kIsWeb ? 0x20000000000000 : 0x7FFFFFFFFFFFFFFF;
var (wrapHours, wrapMinutes, wrapSeconds) = (maxInteger, maxInteger, maxInteger);

if (definition.input.days.output) {
wrapHours = Duration.hoursPerDay;
wrapMinutes = Duration.minutesPerDay;
wrapSeconds = Duration.secondsPerDay;
yield value.inDays + fractionalDays;
}
if (definition.input.hours.output) {
wrapMinutes = Duration.minutesPerHour;
wrapSeconds = Duration.secondsPerHour;
yield (value.inHours % wrapHours) + fractionalHours;
}
if (definition.input.minutes.output) {
wrapSeconds = Duration.secondsPerMinute;
yield (value.inMinutes % wrapMinutes) + fractionalMinutes;
}
if (definition.input.seconds.output) {
yield value.inSeconds % wrapSeconds;
}
}

@override
Expand Down
Loading
Loading