From 9ef0135cb5a59bd93d6ce6c5db0e206c684f9f57 Mon Sep 17 00:00:00 2001 From: ghasem shirdel Date: Fri, 5 Nov 2021 23:14:34 +0330 Subject: [PATCH 1/4] Add precondition function --- lib/dartx.dart | 1 + lib/src/preconditions.dart | 22 ++++++++++++++++++++++ test/preconditions_test.dart | 26 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 lib/src/preconditions.dart create mode 100644 test/preconditions_test.dart diff --git a/lib/dartx.dart b/lib/dartx.dart index 4966351..213817d 100644 --- a/lib/dartx.dart +++ b/lib/dartx.dart @@ -22,6 +22,7 @@ part 'src/iterable_num.dart'; part 'src/list.dart'; part 'src/map.dart'; part 'src/num.dart'; +part 'src/preconditions.dart'; part 'src/range.dart'; part 'src/sorted_list.dart'; part 'src/string.dart'; diff --git a/lib/src/preconditions.dart b/lib/src/preconditions.dart new file mode 100644 index 0000000..cf93f4d --- /dev/null +++ b/lib/src/preconditions.dart @@ -0,0 +1,22 @@ +part of dartx; + +/// Throws an [Exception] with the result of calling [message] if the [value] is false. +// ignore: avoid_positional_boolean_parameters +void require(bool value, [dynamic message = 'Failed requirement.']) { + if (!value) { + throw Exception(message); + } +} + +/// Throws an [Exception] with the result of calling [message] if the [value] is null. Otherwise +/// returns the not null value. +T requireNotNull(T? value, [dynamic message = 'Required value was null.']) { + if (value == null) { + throw Exception(message); + } else { + return value; + } +} + +/// Throws an [IllegalStateException] with the given [message]. +void error(dynamic message) => throw Exception(message); diff --git a/test/preconditions_test.dart b/test/preconditions_test.dart new file mode 100644 index 0000000..1e2dfc3 --- /dev/null +++ b/test/preconditions_test.dart @@ -0,0 +1,26 @@ +import 'package:dartx/dartx_io.dart'; +import 'package:test/test.dart'; + +void main() { + group('Preconditions', () { + test('require() requirements not met', () { + expect(() => require(false), throwsException); + }); + + test('require() requirements met', () { + expect(() => require(true), returnsNormally); + }); + + test('requireNotNull() value is null and requirements not met', () { + expect(() => requireNotNull(null), throwsException); + }); + + test('requireNotNull() value is not null and requirements met', () { + expect(requireNotNull('value'), 'value'); + }); + + test('error()', () { + expect(() => error('error massage'), throwsException); + }); + }); +} From e38fc450447df4a14c860cdfbdaf1e0d8701d02b Mon Sep 17 00:00:00 2001 From: ghasem shirdel Date: Sat, 6 Nov 2021 00:07:07 +0330 Subject: [PATCH 2/4] Add extension function for Random class --- lib/dartx.dart | 1 + lib/src/random.dart | 207 +++++++++++++++++++++++++++++++++++ test/preconditions_test.dart | 2 +- test/random_test.dart | 153 ++++++++++++++++++++++++++ 4 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 lib/src/random.dart create mode 100644 test/random_test.dart diff --git a/lib/dartx.dart b/lib/dartx.dart index 213817d..e39cf57 100644 --- a/lib/dartx.dart +++ b/lib/dartx.dart @@ -23,6 +23,7 @@ part 'src/list.dart'; part 'src/map.dart'; part 'src/num.dart'; part 'src/preconditions.dart'; +part 'src/random.dart'; part 'src/range.dart'; part 'src/sorted_list.dart'; part 'src/string.dart'; diff --git a/lib/src/random.dart b/lib/src/random.dart new file mode 100644 index 0000000..7b60a09 --- /dev/null +++ b/lib/src/random.dart @@ -0,0 +1,207 @@ +part of dartx; + +final _rand = Random(); + +extension RandomNextIntRange on Random { + /// Gets the next random `int` from the random number generator in the specified [range]. + /// + /// Generates an `int` random value uniformly distributed in the specified [range] : + /// from `range.first` inclusive to `range.endInclusive` inclusive with `range.stepSize`. + /// + /// * [throws] IllegalArgumentException if [range] is empty. + /// + /// #### Example : + /// + /// ```dart + /// final rand = Random(); + /// final randomNumber = rand.nextIntRange((-10).rangeTo(10).step(2))); + /// // print a number from [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10] + /// ``` + int nextIntRange(IntProgression range) { + final start = range.start; + final end = range.endInclusive; + final step = range.stepSize; + + _checkRangeBounds(start, end); + return nextInt((end + step - start) ~/ step) * step + start; + } +} + +extension RandomNextDoubleRange on Random { + /// Gets the next random `double` from the random number generator in the specified range. + /// + /// Generates a `double` random value uniformly distributed between the specified [start] (inclusive) and [end] (exclusive) bounds. + /// + /// [start] and [end] must be finite otherwise the behavior is unspecified. + /// + /// * [throws] IllegalArgumentException if [start] is greater than or equal to [end]. + /// + /// #### Example : + /// + /// ```dart + /// final rand = Random(); + /// final randomNumber = rand.nextDoubleRange(10, 11); + /// // a random number in range 10.0..10.99 + /// ``` + double nextDoubleRange(double start, double end) { + _checkRangeBounds(start, end); + + final size = end - start; + final double r; + if (size.isInfinite && start.isFinite && end.isFinite) { + final r1 = nextDouble() * (end / 2 - start / 2); + r = start + r1 + r1; + } else { + r = start + nextDouble() * size; + } + return r >= end ? end.roundToDouble() : r; + } +} + +void _checkRangeBounds(num start, num end) => + require(end > start, 'Cannot get random in empty range: $start..$end'); + +extension RandomChoice on Random { + /// Returns a random element from this list + /// + /// #### Example : + /// + /// ```dart + /// final rand = Random(); + /// final randomNumber = rand.choice([1, 2, 3, 4, 5, 6, 7, 8, 9]); + /// // choice a number from [1, 2, 3, 4, 5, 6, 7, 8, 9] + /// ``` + T choice(List elements) => elements[nextInt(elements.length)]; +} + +extension RandomChoices on Random { + /// Returns a list with the randomly selected element from the specified sequence + /// + /// [weights] Optional. a list were you can weigh the possibility for each value. default null + /// + /// [length] Optional. an integer defining the length of the returned list + /// + /// #### Example : + /// + /// ```dart + /// final rand = Random(); + /// final randomList = rand.choices([1, 2, 3, 4, 5], weights: [100, 1, 1, 1, 100], length: 4); + /// // create a list like that [1, 5, 5, 1] + /// ``` + List choices(List elements, {List? weights, int length = 1}) { + if (weights != null) { + require( + weights.length == elements.length, + 'Weights size must be equals to numbers size', + ); + } + final randoms = []; + + if (weights != null) { + final random = _RandomWeight(weights); + for (var i = 0; i < length; i++) { + final index = random.pickIndex(); + randoms.add(elements[index]); + } + } else { + for (var i = 0; i < length; i++) { + randoms.add(elements.choice()); + } + } + return randoms; + } +} + +extension IterableChoice on Iterable { + /// Returns a random element from this [Iterable] + /// + /// #### Example : + /// + /// ```dart + /// final randomNumber = [1, 2, 3, 4, 5, 6, 7, 8, 9].choice(); + /// // choice a number from [1, 2, 3, 4, 5, 6, 7, 8, 9] + /// ``` + T choice([Random? random]) => (random ?? _rand).choice(toList()); +} + +extension IterableChoices on Iterable { + /// Returns a list with the randomly selected element from the specified sequence + /// + /// [weights] Optional. a list were you can weigh the possibility for each value. default null + /// + /// [length] Optional. an integer defining the length of the returned list + /// + /// #### Example : + /// + /// ```dart + /// final randomList = [1, 2, 3, 4, 5].choices(weights: [100, 1, 1, 1, 100], length: 4); + /// // create a list like that [1, 5, 5, 1] + /// ``` + List choices({Random? random, List? weights, int length = 1}) => + (random ?? _rand).choices(toList(), weights: weights, length: length); +} + +extension MapChoice on Map { + /// Returns a random element from this [Map] + /// + /// #### Example : + /// + /// ```dart + /// final randomNumber = {1: 'a', 2: 'b', 3: 'c'}.choice(); + /// // choice a number from {1: 'a', 2: 'b', 3: 'c'} + /// ``` + Pair choice([Random? random]) { + final choice = (random ?? _rand).choice(keys.toList()); + return Pair(choice, this[choice] as V); + } +} + +class _RandomWeight { + _RandomWeight(List weights) { + _summedWt = List.filled(weights.length, 0); + _summedWt[0] = weights[0]; + + for (final index in weights.indices) { + _upBound += weights[index]; + if (index >= 1) { + _summedWt[index] = weights[index] + _summedWt[index - 1]; + } + } + } + + var _summedWt = []; + var _upBound = 0; + + int pickIndex() { + // Generate a random index between 1 and upBound (both inclusive) + final key = _rand.nextInt(_upBound) + 1; + + // Search for the key in the sorted summedWt array + return _binSearch(key); + } + + int _binSearch(int key) { + var lo = 0; + var hi = _summedWt.length - 1; + var mid = (lo + (hi - lo) / 2).toInt(); + + while (lo < hi) { + // If the key belongs to the current bucket + if (_summedWt[mid] == key || + _summedWt[mid] > key && mid - 1 >= 0 && _summedWt[mid - 1] < key) { + break; + } else if (_summedWt[mid] < key && _summedWt[mid + 1] >= key) { + mid += 1; + break; + } else if (_summedWt[mid] < key) { + lo = mid + 1; + } else { + hi = mid - 1; + } + + // Compute the new mid + mid = (lo + (hi - lo) / 2).toInt(); + } + return mid; + } +} diff --git a/test/preconditions_test.dart b/test/preconditions_test.dart index 1e2dfc3..7cbf69f 100644 --- a/test/preconditions_test.dart +++ b/test/preconditions_test.dart @@ -1,4 +1,4 @@ -import 'package:dartx/dartx_io.dart'; +import 'package:dartx/dartx.dart'; import 'package:test/test.dart'; void main() { diff --git a/test/random_test.dart b/test/random_test.dart new file mode 100644 index 0000000..9487f02 --- /dev/null +++ b/test/random_test.dart @@ -0,0 +1,153 @@ +import 'dart:math'; + +import 'package:dartx/dartx.dart'; +import 'package:test/test.dart'; + +void main() { + final random = Random(); + final secureRandom = Random.secure(); + + group('Random', () { + test('.nextIntRange() range not empty', () { + for (var i = 0; i < 100; i++) { + final randomNumber = random.nextIntRange((-50).rangeTo(50).step(5)); + expect((-50).rangeTo(50).step(5).contains(randomNumber), true); + } + }); + + test('.nextIntRange() range is empty', () { + expect(() => random.nextIntRange(10.rangeTo(0)), throwsException); + expect(() => random.nextIntRange(0.rangeTo(0)), throwsException); + }); + + test('.nextDoubleRange() range not empty', () { + for (var i = 0; i < 100; i++) { + final randomNumber = random.nextDoubleRange(10.0, 15.0); + expect(10.0.rangeTo(15.0).contains(randomNumber), true); + } + }); + + test('.nextDoubleRange() range is empty', () { + expect(() => random.nextDoubleRange(10.0, 9.0), throwsException); + expect(() => random.nextDoubleRange(0.0, 0.0), throwsException); + }); + + test('.choice()', () { + final list = [1, 3, 51, 2, 6, 2, 5]; + for (var i = 0; i < 100; i++) { + final randomNumber = random.choice(list); + expect(list.contains(randomNumber), true); + } + }); + + test('.choices() without weights', () { + final list = [1, 3, 51, 2, 6, 2, 5, 8, 36, 85]; + for (var i = 0; i < 100; i++) { + final randomNumbers = random.choices(list, length: 5); + expect(randomNumbers.any((element) => list.contains(element)), true); + } + }); + + test('.choices() with weights', () { + final list = [6, 32, 41, 52, 3, 7, 9, 2, 6, 0, -5, -74]; + for (var i = 0; i < 100; i++) { + final randomNumbers = random.choices( + list, + weights: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], + length: 5, + ); + expect(randomNumbers.any((element) => list.contains(element)), true); + } + }); + + test('.choices() with invalid weights', () { + expect(() => random.choices([1, 2, 3], weights: [1, 5]), throwsException); + }); + }); + + group('Iterable', () { + test('.choice(Random)', () { + final list = [1, 3, 51, 2, 6, 2, 5, 8, 36, 0]; + for (var i = 0; i < 100; i++) { + final randomNumber = list.choice(); + expect(list.contains(randomNumber), true); + } + }); + + test('.choice(Random.secure())', () { + final list = [1, 3, 51, 2, 6, 2, 5, 8, 36, 0]; + for (var i = 0; i < 100; i++) { + final randomNumber = list.choice(secureRandom); + expect(list.contains(randomNumber), true); + } + }); + + test('.choices(Random) without weights', () { + final list = [1, 3, 51, 2, 6, 2, 5, 8, 36, 0]; + for (var i = 0; i < 100; i++) { + final randomNumbers = list.choices(length: 3); + expect(randomNumbers.any((element) => list.contains(element)), true); + } + }); + + test('.choices(Random) with weights', () { + final list = [1, 2, 3, 4, 5, 6]; + for (var i = 0; i < 100; i++) { + final randomNumbers = + list.choices(weights: [1, 1, 2, 1, 7, 1], length: 3); + expect(randomNumbers.any((element) => list.contains(element)), true); + } + }); + + test('.choices(Random) with invalid weights', () { + expect(() => [1, 2, 3].choices(weights: [1, 5]), throwsException); + }); + + test('.choices(Random.secure()) without weights', () { + final list = [1, 3, 51, 2, 6, 2, 5, 8, 36, 0]; + for (var i = 0; i < 100; i++) { + final randomNumbers = list.choices(random: secureRandom, length: 3); + expect(randomNumbers.any((element) => list.contains(element)), true); + } + }); + + test('.choices(Random.secure()) with weights', () { + final list = [1, 2, 3, 4, 5, 6]; + for (var i = 0; i < 100; i++) { + final randomNumbers = list.choices( + random: secureRandom, + weights: [1, 1, 2, 1, 7, 1], + length: 3, + ); + expect(randomNumbers.any((element) => list.contains(element)), true); + } + }); + + test('.choices(Random.secure()) with invalid weights', () { + expect( + () => [1, 2, 3].choices(random: secureRandom, weights: [1, 5]), + throwsException, + ); + }); + }); + + group('Map', () { + test('.choice(Random)', () { + final map = {1: 'a', 2: 'b', 3: 'c', 4: 'd'}; + for (var i = 0; i < 100; i++) { + final randomPair = map.choice(); + expect(map.containsKey(randomPair.first), true); + expect(map.containsValue(randomPair.second), true); + } + }); + + test('.choice(Random.secure())', () { + final map = {1: 'a', 2: 'b', 3: 'c', 4: 'd'}; + for (var i = 0; i < 100; i++) { + final randomPair = map.choice(secureRandom); + expect(map.containsKey(randomPair.first), true); + expect(map.containsValue(randomPair.second), true); + } + }); + }); +} From 620ef544f2b88793d6655629fdd94cae388502b9 Mon Sep 17 00:00:00 2001 From: ghasem shirdel Date: Sat, 6 Nov 2021 15:45:08 +0330 Subject: [PATCH 3/4] Update document --- lib/src/preconditions.dart | 2 +- lib/src/random.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/preconditions.dart b/lib/src/preconditions.dart index cf93f4d..4172076 100644 --- a/lib/src/preconditions.dart +++ b/lib/src/preconditions.dart @@ -18,5 +18,5 @@ T requireNotNull(T? value, [dynamic message = 'Required value was null.']) { } } -/// Throws an [IllegalStateException] with the given [message]. +/// Throws an [Exception] with the given [message]. void error(dynamic message) => throw Exception(message); diff --git a/lib/src/random.dart b/lib/src/random.dart index 7b60a09..4c15a3b 100644 --- a/lib/src/random.dart +++ b/lib/src/random.dart @@ -41,7 +41,7 @@ extension RandomNextDoubleRange on Random { /// ```dart /// final rand = Random(); /// final randomNumber = rand.nextDoubleRange(10, 11); - /// // a random number in range 10.0..10.99 + /// // a random number in range [10.0..11) /// ``` double nextDoubleRange(double start, double end) { _checkRangeBounds(start, end); From 5d0d3ba63a98362bad707ff155e4b6700086d9e7 Mon Sep 17 00:00:00 2001 From: ghasem shirdel Date: Sat, 6 Nov 2021 15:45:36 +0330 Subject: [PATCH 4/4] Update README.md --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 85b5251..c8fdef0 100644 --- a/README.md +++ b/README.md @@ -364,6 +364,48 @@ for (final i in 10.rangeTo(2).step(2)) { } ``` +## Random + +### .nextIntRange() + +Gets the next random `int` from the random number generator in the specified range. + +```dart +final rand = Random(); +final randomNumber = rand.nextIntRange((-10).rangeTo(10).step(2))); +// print a number from [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10] +``` + +### .nextDoubleRange() + +Gets the next random `double` from the random number generator in the specified range. + +```dart +final rand = Random(); +final randomNumber = rand.nextDoubleRange(10, 11); +// a random number in range [10.0..11) +``` + +### .choice() + +Returns a random element from this list + +```dart +final rand = Random(); +final randomNumber = rand.choice([1, 2, 3, 4, 5, 6, 7, 8, 9]); +// choice a number from [1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +### .choices() + +Returns a list with the randomly selected element from the specified sequence + + ```dart +final rand = Random(); +final randomList = rand.choices([1, 2, 3, 4, 5], weights: [100, 1, 1, 1, 100], length: 4); +// create a list like that [1, 5, 5, 1] +``` + ## Function ### .invoke() - DEPRECATED