-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
231 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
tags: | ||
flaky: | ||
retry: 3 | ||
retry: 3 | ||
property: | ||
skip: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
abstract class Sil<E> with Iterable<E> { | ||
|
||
final Map<String, E> _map; | ||
final List<E> _list; | ||
|
||
|
||
} | ||
|
||
void a() { | ||
f = [].indexed; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import 'dart:math'; | ||
|
||
import 'package:sugar/sugar.dart'; | ||
|
||
/// Provides low-level functions for manipulating indexes in a String Indexed List (SIL). | ||
/// | ||
/// Users should generally prefer the higher-level [SIL] instead. | ||
/// | ||
/// ## Description | ||
/// SIL indexes are strings compared alphabetically to determine order. For example, 'a' is ordered before 'b' since | ||
/// `'a' < 'b'`. Each character in a SIL index is one of the allowed 64 characters, `+, -, [0-9], [A-Z] and [a-z]`. | ||
/// | ||
/// If two indexes contain different number of characters, the shorter index will be implicitly suffixed with `+`s | ||
/// (the first allowed character) until its length is equal to the longer index. For example, when comparing `a` and | ||
/// `a+a`, `a` will be implicated suffixed as `a++`. | ||
/// | ||
/// This guarantees that an element can always be inserted by suffixing its index with an allowed character. For example, | ||
/// `aa` can be inserted between `a` and `b`. | ||
/// | ||
/// It is still possible for two equivalent indexes without any empty space in-between to be generated concurrently. It | ||
/// is impossible for the functions in [SilIndex] to prevent that. Such situations should be handled during merging instead. | ||
/// | ||
/// ## The `strict` flag | ||
/// In the original closed-source implementation, the allowed character set contained `/` instead of `-`. To maintain | ||
/// backwards-compatibility, most functions accept a `strict` flag which disables index format validation. | ||
/// | ||
/// External users are discouraged from enabling the `lenient` flag. | ||
extension SilIndex on Never { | ||
|
||
/// The minimum character. | ||
static const min = '+'; | ||
/// The maximum character. | ||
static const max = 'z'; | ||
/// The allow character set in a SIL index. | ||
static const ascii = [ | ||
// The original implementation used / instead of -, however this made working with URLs/escaping troublesome. | ||
43, 45, // +, - | ||
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, // 0 - 9 | ||
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, // A - Z | ||
97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, // a - z | ||
]; | ||
|
||
static final Random _random = Random(); | ||
static final RegExp _format = RegExp(r'(\+|-|[0-9]|[A-Z]|[a-z])+'); | ||
static final RegExp _trailing = RegExp(r'(\+)+$'); | ||
|
||
/// Generates a new SIL index between the given [min], inclusive, and [max], exclusive. | ||
/// | ||
/// ## The `strict` flag | ||
/// In the original closed-source implementation, the allowed character set contained `/` instead of `-`. To maintain | ||
/// backwards-compatibility, [between] ] accept a [strict] flag which disables index format validation. | ||
/// | ||
/// ## Contract | ||
/// An [ArgumentError] is thrown if | ||
/// * [max] <= [min] | ||
/// * [strict] is true and either [min] or [max] is not a valid SIL index | ||
@Possible({ArgumentError}) | ||
static String between({String min = min, String max = max, bool strict = true}) { | ||
_validate(min, max, strict: strict); | ||
|
||
final index = StringBuffer(); | ||
for (var i = 0; ; i++) { | ||
final first = ascii.indexOf(min.charCodeAt(i, ascii.first)); | ||
final last = ascii.indexOf(max.charCodeAt(i, ascii.last)); | ||
|
||
if (last - first == 0) { | ||
index.writeCharCode(ascii[first]); | ||
continue; | ||
} | ||
|
||
final between = _random.nextBoundedInt(first, (first < last ? last : ascii.length)); | ||
index.writeCharCode(ascii[between]); | ||
|
||
// This detects cases where between is '+' and first is empty as empty characters in the minimum boundary are treated | ||
// as implicit `+`s. | ||
if (between - first != 0) { | ||
return _stripTrailing(index.toString()); | ||
} | ||
} | ||
} | ||
|
||
static void _validate(String min, String max, {required bool strict}) { | ||
if (strict && !min.matches(_format)) { | ||
throw ArgumentError('SIL index, "$min", is invalid. Should be in the format: ([0-9]|[A-Z]|[a-z]|\\+|-)+'); | ||
} | ||
|
||
if (strict && !max.matches(_format)) { | ||
throw ArgumentError('SIL index, "$max", is invalid. Should be in the format: ([0-9]|[A-Z]|[a-z]|\\+|-)+'); | ||
} | ||
|
||
if ((max.replaceAll(_trailing, '')) <= min.replaceAll(_trailing, '') ) { | ||
throw ArgumentError('Minimum SIL index, "$min", is greater than or equal to the maximum SIL index, "$max". Minimum should be less than maximum.'); | ||
} | ||
} | ||
|
||
static String _stripTrailing(String index) { | ||
assert(!index.endsWith('+'), 'SIL index, "$index", contains trailing "+"s.'); | ||
return index.endsWith('+') ? index.replaceAll(_trailing, '') : index; | ||
} | ||
|
||
} | ||
|
||
extension on String { | ||
int charCodeAt(int index, int defaultValue) => index < length ? codeUnitAt(index) : defaultValue; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import 'dart:math'; | ||
|
||
import 'package:sugar/src/crdt/sil_index.dart'; | ||
import 'package:sugar/sugar.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
final _random = Random(); | ||
final _index = StringBuffer(); | ||
|
||
Iterable<(String, String)> get boundaries sync* { | ||
const iterations = 2000; // Tweak this to adjust the number of tests | ||
for (var i = 0; i < iterations; i++) { | ||
var min = generate(); | ||
var max = generate(); | ||
while (min.isEmpty || max.isEmpty || min >= max) { | ||
min = generate(); | ||
max = generate(); | ||
} | ||
|
||
yield (min, max); | ||
} | ||
} | ||
|
||
String generate() { | ||
final length = _random.nextInt(8) + 1; | ||
for (var i = 0; i < length; i++) { | ||
_index.writeCharCode(SilIndex.ascii[_random.nextInt(SilIndex.ascii.length)]); | ||
} | ||
|
||
final index = _index.toString().replaceAll(RegExp(r'(\+)+$'), ''); | ||
_index.clear(); | ||
|
||
return index; | ||
} | ||
|
||
|
||
void main() { | ||
group('preconditions', () { | ||
for (final (min, max) in [ | ||
('b', 'a'), | ||
('a', 'a'), | ||
('a++++', 'a++'), | ||
('a++', 'a+++++++'), | ||
('a', 'a++++'), | ||
('a++++++', 'a'), | ||
]) { | ||
test('start index >= end index', () => expect( | ||
() => SilIndex.between(min: min, max: max), | ||
throwsA(predicate<ArgumentError>( | ||
(e) => e.message == 'Minimum SIL index, "$min", is greater than or equal to the maximum SIL index, "$max". Minimum should be less than maximum.' | ||
)), | ||
)); | ||
} | ||
|
||
for (final argument in ['1241=', '20"385r2', '漢字']) { | ||
test('invalid format', () => expect( | ||
() => SilIndex.between(min: argument), | ||
throwsA(predicate<ArgumentError>( | ||
(e) => e.message == 'SIL index, "$argument", is invalid. Should be in the format: ([0-9]|[A-Z]|[a-z]|\\+|-)+' | ||
)), | ||
)); | ||
|
||
test('invalid format', () => expect( | ||
() => SilIndex.between(max: argument), | ||
throwsA(predicate<ArgumentError>( | ||
(e) => e.message == 'SIL index, "$argument", is invalid. Should be in the format: ([0-9]|[A-Z]|[a-z]|\\+|-)+' | ||
)), | ||
)); | ||
} | ||
}); | ||
|
||
test('wrap around', () { | ||
final value = SilIndex.between(min: '+yzz', max: '-'); | ||
expect(value > '+yzz', true); | ||
expect(value < '-', true); | ||
}); | ||
|
||
test('boundary', () { | ||
final value = SilIndex.between(min: '+zzz', max: '-'); | ||
expect(value.compareTo('+zzz'), 1); | ||
expect(value.compareTo('/'), -1); | ||
}); | ||
|
||
for (final (min, max) in boundaries) { | ||
test('insert between $min and $max', () { | ||
final value = SilIndex.between(min: min, max: max); | ||
expect(value.endsWith('+'), false); | ||
expect(value.compareTo(min), 1, reason: '$value <= $min'); | ||
expect(value.compareTo(max), -1, reason: '$value >= $max'); | ||
}, tags: ['property']); | ||
} | ||
} |