Skip to content

Instantly share code, notes, and snippets.

@bergwerf
Created March 6, 2016 18:31
Show Gist options
  • Save bergwerf/48a858042a7a98405575 to your computer and use it in GitHub Desktop.
Save bergwerf/48a858042a7a98405575 to your computer and use it in GitHub Desktop.
String set generator
// Copyright (c) 2016, Herman Bergwerf. All rights reserved.
/// String set generator
///
/// Generates a set of strings from an input string that contains optional
/// fragments. An optional fragment is written using square brackets. If a
/// fragment itself has multiple alternatives, you can use a vertical bar
/// character to separate them. If the final strings should all include at least
/// one of the alternatives provided by the square brackets, you should add a
/// exclamation mark before the opening bracket. Square brackets, vertical bars
/// and exclamation can be escaped using a backslash to special behaviour (make
/// sure to pass a literal string in this case). You can also nest this syntax.
/// An example is provided below.
///
/// `generateStrings('This[ function]![ can be| is[ being]] used for stuff.')`
Set<String> generateStrings(String str) {
return _evaluateStringFragment(str).strings;
}
/// Return value for [_evaluateStringFragment]
class _EvaluatedStringFragment {
Set<String> strings;
int lastIndex;
_EvaluatedStringFragment(this.strings, this.lastIndex);
}
/// Recursive subfunction for generateStrings
_EvaluatedStringFragment _evaluateStringFragment(String str) {
var strings = new List<Set<String>>();
strings.add(new Set<String>());
var activeString = '';
// This variable is incremented at the start of each cycle so if the previous
// character is an exclamation mark then exclamationMarkSet is 1.
var lastExclamationOffset = 1;
// Loop through characters.
var i = 0;
for (; i < str.length; i++) {
lastExclamationOffset++;
// Check if this is escaped.
if (i > 0 && str[i - 1] == r'\') {
// Remove backslash and move index backward.
str.replaceRange(i - 1, i, '');
i--;
// Push character into current activeString.
activeString = activeString + str[i];
break;
}
switch (str[i]) {
// New fragment
case '[':
// Add accumulated activeString to all strings in this set.
strings[strings.length - 1] =
_addEachToAll([activeString], strings.last, false);
activeString = '';
// Evaluate fragment.
var fragment = _evaluateStringFragment(str.substring(i + 1));
// Add results to current set.
strings[strings.length - 1] = _addEachToAll(
fragment.strings, strings.last, lastExclamationOffset != 1);
// Move index forward.
i += fragment.lastIndex + 1;
break;
// Close this fragment.
case ']':
// Add accumulated activeString to all strings in this set.
strings[strings.length - 1] =
_addEachToAll([activeString], strings.last, false);
// Condense strings into a single set.
var allStrings = new Set<String>();
strings.forEach((Set<String> strs) {
allStrings.addAll(strs);
});
return new _EvaluatedStringFragment(allStrings, i);
// Start alternative pattern.
case '|':
// Add accumulated activeString to all strings in this set.
strings[strings.length - 1] =
_addEachToAll([activeString], strings.last, false);
activeString = '';
// Add new strings set for alternative pattern.
strings.add(new Set<String>());
break;
// Notify next cycle that there is an exclamation mark (the fragment that
// follows is not optional).
case '!':
lastExclamationOffset = 0;
break;
default:
// Push character into current activeString.
activeString = activeString + str[i];
}
}
// No terminating ']' found, so return what we have.
// Add accumulated activeString to all strings in this set.
strings[strings.length - 1] =
_addEachToAll([activeString], strings.last, false);
// Condense strings into a single set.
var allStrings = new Set<String>();
strings.forEach((Set<String> strs) {
allStrings.addAll(strs);
});
return new _EvaluatedStringFragment(allStrings, i);
}
Set<String> _addEachToAll(
Iterable<String> each, Iterable<String> all, bool leaveOneOriginal) {
if (all.length == 0) {
// No existing element in all, so return new element in each.
return each.toSet();
} else {
var strings = new Set<String>();
// Add each possible combination to strings.
for (var _all in all) {
// Add one original copy if desired.
if (leaveOneOriginal) {
strings.add(_all);
}
for (var _each in each) {
strings.add(_all + _each);
}
}
return strings;
}
}
// Copyright (c) 2016, Herman Bergwerf. All rights reserved.
import 'package:test/test.dart';
main() {
test('Test generateStrings function', () {
var strings = generateStrings(
'This[ function]![ can be| is[ being]] used for stuff.');
var expectedStrings = [
'This can be used for stuff.',
'This is used for stuff.',
'This is being used for stuff.',
'This function can be used for stuff.',
'This function is used for stuff.',
'This function is being used for stuff.'
];
// Compare number of strings with the size of the intersection between
// strings and expectedStrings.
expect(expectedStrings.toSet().intersection(strings).length,
equals(expectedStrings.length));
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment