From 72c212e366478a5378f13020ffb18b6e4668e546 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:46:57 +0000 Subject: [PATCH 1/6] Initial plan From df849911bb794d5f46e3367798336af647398815 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:53:29 +0000 Subject: [PATCH 2/6] Add Iterable/List, Map, Set, and Humanize extensions Co-authored-by: YoungMayor <47315212+YoungMayor@users.noreply.github.com> --- lib/src/extensions.dart | 4 + lib/src/extensions/humanize.dart | 226 ++++++++++++++ lib/src/extensions/list.dart | 453 +++++++++++++++++++++++++++++ lib/src/extensions/map.dart | 169 +++++++++++ lib/src/extensions/set.dart | 72 +++++ pubspec.yaml | 2 +- test/extensions/humanize_test.dart | 122 ++++++++ test/extensions/list_test.dart | 242 +++++++++++++++ test/extensions/map_test.dart | 79 +++++ test/extensions/set_test.dart | 48 +++ 10 files changed, 1416 insertions(+), 1 deletion(-) create mode 100644 lib/src/extensions/humanize.dart create mode 100644 lib/src/extensions/list.dart create mode 100644 lib/src/extensions/map.dart create mode 100644 lib/src/extensions/set.dart create mode 100644 test/extensions/humanize_test.dart create mode 100644 test/extensions/list_test.dart create mode 100644 test/extensions/map_test.dart create mode 100644 test/extensions/set_test.dart diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 811ca07..ea9860a 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -17,9 +17,13 @@ part './extensions/build_context.dart'; part './extensions/date_time.dart'; part './extensions/duration.dart'; part './extensions/dynamic.dart'; +part './extensions/humanize.dart'; part './extensions/image_widget.dart'; +part './extensions/list.dart'; +part './extensions/map.dart'; part './extensions/number.dart'; part './extensions/object.dart'; +part './extensions/set.dart'; part './extensions/stateless_widget.dart'; part './extensions/string.dart'; part './extensions/text_style.dart'; diff --git a/lib/src/extensions/humanize.dart b/lib/src/extensions/humanize.dart new file mode 100644 index 0000000..c4ccb2f --- /dev/null +++ b/lib/src/extensions/humanize.dart @@ -0,0 +1,226 @@ +part of './../extensions.dart'; + +extension MayrDurationHumanizeExtensions on Duration { + /// Converts the duration to a human-readable string. + /// + /// Example: + /// ```dart + /// Duration(hours: 2, minutes: 3).humanize(); // '2 hours, 3 minutes' + /// Duration(days: 1).humanize(); // '1 day' + /// Duration(seconds: 45).humanize(); // '45 seconds' + /// ``` + String humanize({String locale = 'en'}) { + final days = inDays; + final hours = inHours.remainder(24); + final minutes = inMinutes.remainder(60); + final seconds = inSeconds.remainder(60); + + final parts = []; + + if (days > 0) { + parts.add('$days ${days == 1 ? 'day' : 'days'}'); + } + if (hours > 0) { + parts.add('$hours ${hours == 1 ? 'hour' : 'hours'}'); + } + if (minutes > 0) { + parts.add('$minutes ${minutes == 1 ? 'minute' : 'minutes'}'); + } + if (seconds > 0 && days == 0 && hours == 0) { + parts.add('$seconds ${seconds == 1 ? 'second' : 'seconds'}'); + } + + if (parts.isEmpty) return '0 seconds'; + if (parts.length == 1) return parts[0]; + if (parts.length == 2) return '${parts[0]}, ${parts[1]}'; + + return parts.sublist(0, parts.length - 1).join(', ') + ', and ${parts.last}'; + } +} + +extension MayrDateTimeHumanizeExtensions on DateTime { + /// Converts the date time to a human-readable relative time string. + /// + /// Example: + /// ```dart + /// DateTime.now().humanize(); // 'just now' + /// DateTime.now().subtract(Duration(hours: 3)).humanize(); // '3 hours ago' + /// DateTime.now().add(Duration(days: 2)).humanize(); // '2 days from now' + /// ``` + String humanize({String locale = 'en'}) { + final now = DateTime.now(); + final difference = now.difference(this); + final isFuture = difference.isNegative; + final absDifference = difference.abs(); + + if (absDifference.inSeconds < 30) { + return 'just now'; + } + + if (absDifference.inMinutes < 1) { + return isFuture ? 'in a few seconds' : 'a few seconds ago'; + } + + if (absDifference.inMinutes < 60) { + final minutes = absDifference.inMinutes; + if (minutes == 1) { + return isFuture ? 'in 1 minute' : '1 minute ago'; + } + return isFuture ? 'in $minutes minutes' : '$minutes minutes ago'; + } + + if (absDifference.inHours < 24) { + final hours = absDifference.inHours; + if (hours == 1) { + return isFuture ? 'in 1 hour' : '1 hour ago'; + } + return isFuture ? 'in $hours hours' : '$hours hours ago'; + } + + if (absDifference.inDays == 1) { + return isFuture ? 'tomorrow' : 'yesterday'; + } + + if (absDifference.inDays < 7) { + final days = absDifference.inDays; + return isFuture ? 'in $days days' : '$days days ago'; + } + + if (absDifference.inDays < 14) { + return isFuture ? 'next week' : 'last week'; + } + + if (absDifference.inDays < 30) { + final weeks = (absDifference.inDays / 7).floor(); + return isFuture ? 'in $weeks weeks' : '$weeks weeks ago'; + } + + if (absDifference.inDays < 60) { + return isFuture ? 'next month' : 'last month'; + } + + if (absDifference.inDays < 365) { + final months = (absDifference.inDays / 30).floor(); + return isFuture ? 'in $months months' : '$months months ago'; + } + + final years = (absDifference.inDays / 365).floor(); + if (years == 1) { + return isFuture ? 'next year' : 'last year'; + } + return isFuture ? 'in $years years' : '$years years ago'; + } +} + +extension MayrNumHumanizeExtensions on num { + /// Converts the number to a human-readable compact format. + /// + /// Example: + /// ```dart + /// 1234.humanizeNumber(); // '1.2k' + /// 1500000.humanizeNumber(); // '1.5M' + /// 999.humanizeNumber(); // '999' + /// ``` + String humanizeNumber({int decimals = 1}) { + if (abs() < 1000) return toString(); + + final suffixes = ['', 'k', 'M', 'B', 'T']; + var index = 0; + var value = toDouble(); + + while (value.abs() >= 1000 && index < suffixes.length - 1) { + value /= 1000; + index++; + } + + if (value == value.roundToDouble()) { + return '${value.toInt()}${suffixes[index]}'; + } + + return '${value.toStringAsFixed(decimals)}${suffixes[index]}'; + } + + /// Converts the number to an ordinal string (1st, 2nd, 3rd, etc.). + /// + /// Example: + /// ```dart + /// 1.humanizeOrdinal(); // '1st' + /// 2.humanizeOrdinal(); // '2nd' + /// 3.humanizeOrdinal(); // '3rd' + /// 21.humanizeOrdinal(); // '21st' + /// ``` + String humanizeOrdinal() { + final n = toInt(); + if (n % 100 >= 11 && n % 100 <= 13) { + return '${n}th'; + } + + switch (n % 10) { + case 1: + return '${n}st'; + case 2: + return '${n}nd'; + case 3: + return '${n}rd'; + default: + return '${n}th'; + } + } + + /// Converts the number to a count with singular/plural item name. + /// + /// Example: + /// ```dart + /// 1.humanizeCount('item'); // '1 item' + /// 3.humanizeCount('item'); // '3 items' + /// 0.humanizeCount('item'); // '0 items' + /// ``` + String humanizeCount(String itemName, {String? pluralName}) { + final count = toInt(); + final name = count == 1 ? itemName : (pluralName ?? '${itemName}s'); + return '$count $name'; + } + + /// Converts the number to a percentage string. + /// + /// Example: + /// ```dart + /// 0.75.humanizePercentage(); // '75%' + /// 50.humanizePercentage(max: 100); // '50%' + /// ``` + String humanizePercentage({num max = 1, int decimals = 0}) { + final percentage = (this / max * 100); + if (decimals == 0) { + return '${percentage.round()}%'; + } + return '${percentage.toStringAsFixed(decimals)}%'; + } + + /// Converts bytes to a human-readable file size. + /// + /// Example: + /// ```dart + /// 1024.humanizeFileSize(); // '1.0 KB' + /// 1048576.humanizeFileSize(); // '1.0 MB' + /// 520300.humanizeFileSize(); // '508.1 KB' + /// ``` + String humanizeFileSize({int decimals = 1}) { + if (this < 0) return 'Invalid size'; + if (this == 0) return '0 B'; + + const suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + var index = 0; + var size = toDouble(); + + while (size >= 1024 && index < suffixes.length - 1) { + size /= 1024; + index++; + } + + if (index == 0 || size == size.roundToDouble()) { + return '${size.toInt()} ${suffixes[index]}'; + } + + return '${size.toStringAsFixed(decimals)} ${suffixes[index]}'; + } +} diff --git a/lib/src/extensions/list.dart b/lib/src/extensions/list.dart new file mode 100644 index 0000000..adaa74c --- /dev/null +++ b/lib/src/extensions/list.dart @@ -0,0 +1,453 @@ +part of './../extensions.dart'; + +extension MayrIterableExtensions on Iterable { + /// Returns the first element or `null` if the iterable is empty. + /// + /// Example: + /// ```dart + /// [1, 2, 3].firstOrNull(); // 1 + /// [].firstOrNull(); // null + /// ``` + T? firstOrNull() => isEmpty ? null : first; + + /// Returns the last element or `null` if the iterable is empty. + /// + /// Example: + /// ```dart + /// [1, 2, 3].lastOrNull(); // 3 + /// [].lastOrNull(); // null + /// ``` + T? lastOrNull() => isEmpty ? null : last; + + /// Returns the single element that satisfies the predicate, or `null` if no such element exists. + /// + /// Example: + /// ```dart + /// [1, 2, 3].singleWhereOrNull((e) => e == 2); // 2 + /// [1, 2, 3].singleWhereOrNull((e) => e > 5); // null + /// ``` + T? singleWhereOrNull(bool Function(T) predicate) { + T? result; + var found = false; + for (var element in this) { + if (predicate(element)) { + if (found) return null; // More than one match + result = element; + found = true; + } + } + return result; + } + + /// Returns `true` if at least one element satisfies the predicate. + /// + /// Example: + /// ```dart + /// [1, 2, 3].containsWhere((e) => e > 2); // true + /// [1, 2, 3].containsWhere((e) => e > 5); // false + /// ``` + bool containsWhere(bool Function(T) predicate) => any(predicate); + + /// Returns the index of the first element that satisfies the predicate, or `null` if not found. + /// + /// Example: + /// ```dart + /// [1, 2, 3].indexWhereOrNull((e) => e == 2); // 1 + /// [1, 2, 3].indexWhereOrNull((e) => e > 5); // null + /// ``` + int? indexWhereOrNull(bool Function(T) predicate) { + var index = 0; + for (var element in this) { + if (predicate(element)) return index; + index++; + } + return null; + } + + /// Maps each element with its index. + /// + /// Example: + /// ```dart + /// ['a', 'b', 'c'].mapIndexed((i, e) => '$i: $e'); // ['0: a', '1: b', '2: c'] + /// ``` + Iterable mapIndexed(R Function(int index, T item) transform) sync* { + var index = 0; + for (var element in this) { + yield transform(index++, element); + } + } + + /// Filters out null values from the iterable. + /// + /// Example: + /// ```dart + /// [1, null, 2, null, 3].whereNotNull(); // [1, 2, 3] + /// ``` + Iterable whereNotNull() => where((e) => e != null); + + /// Returns unique elements based on a key selector. + /// + /// Example: + /// ```dart + /// [Person('Alice', 30), Person('Bob', 25), Person('Alice', 35)] + /// .distinctBy((p) => p.name); // [Person('Alice', 30), Person('Bob', 25)] + /// ``` + Iterable distinctBy(K Function(T) keySelector) { + final seen = {}; + return where((element) => seen.add(keySelector(element))); + } + + /// Returns elements sorted by a key selector. + /// + /// Example: + /// ```dart + /// [Person('Bob', 25), Person('Alice', 30)].sortedBy((p) => p.name); + /// // [Person('Alice', 30), Person('Bob', 25)] + /// ``` + List sortedBy>(K Function(T) keySelector) { + final list = toList(); + list.sort((a, b) => keySelector(a).compareTo(keySelector(b))); + return list; + } + + /// Returns elements sorted by a key selector in descending order. + /// + /// Example: + /// ```dart + /// [Person('Bob', 25), Person('Alice', 30)].sortedByDesc((p) => p.age); + /// // [Person('Alice', 30), Person('Bob', 25)] + /// ``` + List sortedByDesc>(K Function(T) keySelector) { + final list = toList(); + list.sort((a, b) => keySelector(b).compareTo(keySelector(a))); + return list; + } + + /// Counts elements that satisfy the predicate. + /// + /// Example: + /// ```dart + /// [1, 2, 3, 4, 5].countWhere((e) => e > 3); // 2 + /// ``` + int countWhere(bool Function(T) predicate) => + where(predicate).length; + + /// Checks if the iterable is `null` or empty. + /// + /// Example: + /// ```dart + /// [].isNullOrEmpty(); // true + /// [1, 2, 3].isNullOrEmpty(); // false + /// ``` + bool isNullOrEmpty() => isEmpty; + + /// Joins elements to a string with optional separator and transform. + /// + /// Example: + /// ```dart + /// [1, 2, 3].joinToString(separator: ', '); // '1, 2, 3' + /// [1, 2, 3].joinToString(separator: ', ', transform: (e) => 'Item $e'); + /// // 'Item 1, Item 2, Item 3' + /// ``` + String joinToString({ + String separator = '', + String Function(T)? transform, + }) { + if (transform != null) { + return map(transform).join(separator); + } + return join(separator); + } + + /// Executes an action for each element with its index. + /// + /// Example: + /// ```dart + /// [1, 2, 3].forEachIndexed((i, e) => print('$i: $e')); + /// // Prints: '0: 1', '1: 2', '2: 3' + /// ``` + void forEachIndexed(void Function(int index, T item) action) { + var index = 0; + for (var element in this) { + action(index++, element); + } + } +} + +extension MayrListExtensions on List { + /// Returns the element at [index] or `null` if out of bounds. + /// + /// Example: + /// ```dart + /// [1, 2, 3].getOrNull(1); // 2 + /// [1, 2, 3].getOrNull(5); // null + /// ``` + T? getOrNull(int index) { + if (index < 0 || index >= length) return null; + return this[index]; + } + + /// Returns the element at [index] or [defaultValue] if out of bounds. + /// + /// Example: + /// ```dart + /// [1, 2, 3].getOrDefault(1, 0); // 2 + /// [1, 2, 3].getOrDefault(5, 0); // 0 + /// ``` + T getOrDefault(int index, T defaultValue) { + if (index < 0 || index >= length) return defaultValue; + return this[index]; + } + + /// Splits the list into chunks of the specified size. + /// + /// Example: + /// ```dart + /// [1, 2, 3, 4, 5].chunked(2); // [[1, 2], [3, 4], [5]] + /// ``` + List> chunked(int size) { + if (size <= 0) throw ArgumentError('Size must be positive'); + final chunks = >[]; + for (var i = 0; i < length; i += size) { + chunks.add(sublist(i, i + size > length ? length : i + size)); + } + return chunks; + } + + /// Flattens a list of lists into a single list. + /// + /// Example: + /// ```dart + /// [[1, 2], [3, 4], [5]].flatten(); // [1, 2, 3, 4, 5] + /// ``` + List flatten() { + final result = []; + for (var element in this) { + if (element is List) { + result.addAll(element); + } + } + return result; + } + + /// Flips the list (reverses it). + /// + /// Example: + /// ```dart + /// [1, 2, 3].flip(); // [3, 2, 1] + /// ``` + List flip() => reversed.toList(); + + /// Inserts [value] if [condition] is true. + /// + /// Example: + /// ```dart + /// [1, 2, 3].insertIf(true, 4); // [1, 2, 3, 4] + /// [1, 2, 3].insertIf(false, 4); // [1, 2, 3] + /// ``` + List insertIf(bool condition, T value) { + final result = List.from(this); + if (condition) result.add(value); + return result; + } + + /// Replaces elements that satisfy the predicate with [newValue]. + /// + /// Example: + /// ```dart + /// [1, 2, 3, 2].replaceWhere((e) => e == 2, 5); // [1, 5, 3, 5] + /// ``` + List replaceWhere(bool Function(T) predicate, T newValue) { + return map((e) => predicate(e) ? newValue : e).toList(); + } + + /// Removes elements that do not satisfy the predicate. + /// + /// Example: + /// ```dart + /// [1, 2, 3, 4, 5].removeWhereNot((e) => e > 2); // [3, 4, 5] + /// ``` + List removeWhereNot(bool Function(T) predicate) { + return where(predicate).toList(); + } + + /// Updates elements that satisfy the predicate using the updater function. + /// + /// Example: + /// ```dart + /// [1, 2, 3, 4].updateWhere((e) => e > 2, (e) => e * 2); // [1, 2, 6, 8] + /// ``` + List updateWhere(bool Function(T) predicate, T Function(T) updater) { + return map((e) => predicate(e) ? updater(e) : e).toList(); + } + + /// Adds [value] if it's not null. + /// + /// Example: + /// ```dart + /// [1, 2, 3].addIf(4); // [1, 2, 3, 4] + /// [1, 2, 3].addIf(null); // [1, 2, 3] + /// ``` + List addIf(T? value) { + final result = List.from(this); + if (value != null) result.add(value); + return result; + } + + /// Adds all elements from [values] if not null. + /// + /// Example: + /// ```dart + /// [1, 2].addAllIf([3, 4]); // [1, 2, 3, 4] + /// [1, 2].addAllIf(null); // [1, 2] + /// ``` + List addAllIf(Iterable? values) { + final result = List.from(this); + if (values != null) result.addAll(values); + return result; + } + + /// Appends [value] to the list (alias for add, returns new list). + /// + /// Example: + /// ```dart + /// [1, 2, 3].append(4); // [1, 2, 3, 4] + /// ``` + List append(T value) { + final result = List.from(this); + result.add(value); + return result; + } + + /// Appends all [values] to the list (returns new list). + /// + /// Example: + /// ```dart + /// [1, 2].appendAll([3, 4]); // [1, 2, 3, 4] + /// ``` + List appendAll(Iterable values) { + final result = List.from(this); + result.addAll(values); + return result; + } + + /// Appends [value] to the list if condition is true. + /// + /// Example: + /// ```dart + /// [1, 2, 3].appendIf(4, true); // [1, 2, 3, 4] + /// [1, 2, 3].appendIf(4, false); // [1, 2, 3] + /// ``` + List appendIf(T value, bool condition) { + final result = List.from(this); + if (condition) result.add(value); + return result; + } + + /// Appends all [values] to the list if condition is true. + /// + /// Example: + /// ```dart + /// [1, 2].appendAllIf([3, 4], true); // [1, 2, 3, 4] + /// [1, 2].appendAllIf([3, 4], false); // [1, 2] + /// ``` + List appendAllIf(Iterable values, bool condition) { + final result = List.from(this); + if (condition) result.addAll(values); + return result; + } + + /// Removes and returns the last element of the list. + /// + /// Example: + /// ```dart + /// var list = [1, 2, 3]; + /// list.pop(); // Returns 3, list is now [1, 2] + /// ``` + T? pop() { + if (isEmpty) return null; + return removeLast(); + } + + /// Filters the list using the predicate (alias for where). + /// + /// Example: + /// ```dart + /// [1, 2, 3, 4, 5].fliter((e) => e > 2); // [3, 4, 5] + /// ``` + List fliter(bool Function(T) predicate) => where(predicate).toList(); + + /// Returns a list with unique elements. + /// + /// Example: + /// ```dart + /// [1, 2, 2, 3, 3, 4].unique(); // [1, 2, 3, 4] + /// ``` + List unique() => toSet().toList(); +} + +extension MayrNumIterableExtensions on Iterable { + /// Sums elements by applying a selector function. + /// + /// Example: + /// ```dart + /// [Person('Alice', 30), Person('Bob', 25)].sumBy((p) => p.age); // 55 + /// ``` + num sumBy(num Function(num) selector) => + map(selector).fold(0, (a, b) => a + b); + + /// Calculates the average by applying a selector function. + /// + /// Example: + /// ```dart + /// [Person('Alice', 30), Person('Bob', 20)].averageBy((p) => p.age); // 25.0 + /// ``` + double averageBy(num Function(num) selector) { + if (isEmpty) return 0; + return sumBy(selector) / length; + } + + /// Returns the minimum value in the iterable. + /// + /// Example: + /// ```dart + /// [3, 1, 4, 1, 5].min(); // 1 + /// [].min(); // null + /// ``` + num? min() => isEmpty ? null : reduce((a, b) => a < b ? a : b); + + /// Returns the maximum value in the iterable. + /// + /// Example: + /// ```dart + /// [3, 1, 4, 1, 5].max(); // 5 + /// [].max(); // null + /// ``` + num? max() => isEmpty ? null : reduce((a, b) => a > b ? a : b); +} + +extension MayrComparableIterableExtensions on Iterable { + /// Returns the minimum element by comparing with a key selector. + /// + /// Example: + /// ```dart + /// [Person('Alice', 30), Person('Bob', 25)].minBy((p) => p.age); + /// // Person('Bob', 25) + /// ``` + T? minBy>(K Function(T) keySelector) { + if (isEmpty) return null; + return reduce((a, b) => keySelector(a).compareTo(keySelector(b)) < 0 ? a : b); + } + + /// Returns the maximum element by comparing with a key selector. + /// + /// Example: + /// ```dart + /// [Person('Alice', 30), Person('Bob', 25)].maxBy((p) => p.age); + /// // Person('Alice', 30) + /// ``` + T? maxBy>(K Function(T) keySelector) { + if (isEmpty) return null; + return reduce((a, b) => keySelector(a).compareTo(keySelector(b)) > 0 ? a : b); + } +} diff --git a/lib/src/extensions/map.dart b/lib/src/extensions/map.dart new file mode 100644 index 0000000..21982df --- /dev/null +++ b/lib/src/extensions/map.dart @@ -0,0 +1,169 @@ +part of './../extensions.dart'; + +extension MayrMapExtensions on Map { + /// Returns the value for [key] or `null` if the key doesn't exist. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2}.getOrNull('a'); // 1 + /// {'a': 1, 'b': 2}.getOrNull('c'); // null + /// ``` + V? getOrNull(K key) => this[key]; + + /// Returns the value for [key] or [defaultValue] if the key doesn't exist. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2}.getOrDefault('a', 0); // 1 + /// {'a': 1, 'b': 2}.getOrDefault('c', 0); // 0 + /// ``` + V getOrDefault(K key, V defaultValue) => this[key] ?? defaultValue; + + /// Creates a new map with keys transformed by [transform]. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2}.mapKeys((k, v) => k.toUpperCase()); + /// // {'A': 1, 'B': 2} + /// ``` + Map mapKeys(K2 Function(K key, V value) transform) { + return Map.fromEntries( + entries.map((e) => MapEntry(transform(e.key, e.value), e.value)), + ); + } + + /// Creates a new map with values transformed by [transform]. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2}.mapValues((k, v) => v * 2); + /// // {'a': 2, 'b': 4} + /// ``` + Map mapValues(V2 Function(K key, V value) transform) { + return Map.fromEntries( + entries.map((e) => MapEntry(e.key, transform(e.key, e.value))), + ); + } + + /// Creates a new map with only the entries whose keys satisfy [predicate]. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2, 'c': 3}.filterKeys((k) => k != 'b'); + /// // {'a': 1, 'c': 3} + /// ``` + Map filterKeys(bool Function(K key) predicate) { + return Map.fromEntries( + entries.where((e) => predicate(e.key)), + ); + } + + /// Creates a new map with only the entries whose values satisfy [predicate]. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2, 'c': 3}.filterValues((v) => v > 1); + /// // {'b': 2, 'c': 3} + /// ``` + Map filterValues(bool Function(V value) predicate) { + return Map.fromEntries( + entries.where((e) => predicate(e.value)), + ); + } + + /// Creates a new map with keys and values swapped. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2}.invert(); + /// // {1: 'a', 2: 'b'} + /// ``` + Map invert() { + return Map.fromEntries( + entries.map((e) => MapEntry(e.value, e.key)), + ); + } + + /// Merges this map with [otherMap], with entries from [otherMap] taking precedence. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2}.merge({'b': 3, 'c': 4}); + /// // {'a': 1, 'b': 3, 'c': 4} + /// ``` + Map merge(Map otherMap) { + return {...this, ...otherMap}; + } + + /// Merges this map with [otherMap], but only adds entries that don't exist in this map. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2}.mergeIfAbsent({'b': 3, 'c': 4}); + /// // {'a': 1, 'b': 2, 'c': 4} + /// ``` + Map mergeIfAbsent(Map otherMap) { + final result = Map.from(this); + otherMap.forEach((key, value) { + result.putIfAbsent(key, () => value); + }); + return result; + } + + /// Combines this map with [other] using a [combiner] function for conflicting keys. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2}.combine({'b': 3, 'c': 4}, (k, v1, v2) => v1 + v2); + /// // {'a': 1, 'b': 5, 'c': 4} + /// ``` + Map combine( + Map other, + V Function(K key, V value1, V value2) combiner, + ) { + final result = Map.from(this); + other.forEach((key, value) { + if (result.containsKey(key)) { + result[key] = combiner(key, result[key] as V, value); + } else { + result[key] = value; + } + }); + return result; + } + + /// Returns all keys whose values satisfy [predicate]. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2, 'c': 3}.keysWhere((v) => v > 1); + /// // ['b', 'c'] + /// ``` + Iterable keysWhere(bool Function(V value) predicate) { + return entries.where((e) => predicate(e.value)).map((e) => e.key); + } + + /// Returns all values whose keys satisfy [predicate]. + /// + /// Example: + /// ```dart + /// {'a': 1, 'b': 2, 'c': 3}.valuesWhere((k) => k != 'b'); + /// // [1, 3] + /// ``` + Iterable valuesWhere(bool Function(K key) predicate) { + return entries.where((e) => predicate(e.key)).map((e) => e.value); + } + + /// Converts the map to a URL query string. + /// + /// Example: + /// ```dart + /// {'name': 'John', 'age': '30'}.toQueryString(); + /// // 'name=John&age=30' + /// ``` + String toQueryString() { + return entries + .map((e) => '${Uri.encodeComponent(e.key.toString())}=${Uri.encodeComponent(e.value.toString())}') + .join('&'); + } +} diff --git a/lib/src/extensions/set.dart b/lib/src/extensions/set.dart new file mode 100644 index 0000000..85a952b --- /dev/null +++ b/lib/src/extensions/set.dart @@ -0,0 +1,72 @@ +part of './../extensions.dart'; + +extension MayrSetExtensions on Set { + /// Toggles [element] in the set - adds if missing, removes if present. + /// + /// Example: + /// ```dart + /// var set = {1, 2, 3}; + /// set.toggle(2); // {1, 3} + /// set.toggle(4); // {1, 3, 4} + /// ``` + Set toggle(T element) { + final result = Set.from(this); + if (result.contains(element)) { + result.remove(element); + } else { + result.add(element); + } + return result; + } + + /// Returns `true` if this set intersects with [otherSet]. + /// + /// Example: + /// ```dart + /// {1, 2, 3}.intersects({2, 3, 4}); // true + /// {1, 2}.intersects({3, 4}); // false + /// ``` + bool intersects(Set otherSet) => intersection(otherSet).isNotEmpty; + + /// Returns `true` if this set is a subset of [otherSet]. + /// + /// Example: + /// ```dart + /// {1, 2}.isSubsetOf({1, 2, 3}); // true + /// {1, 4}.isSubsetOf({1, 2, 3}); // false + /// ``` + bool isSubsetOf(Set otherSet) => difference(otherSet).isEmpty; + + /// Returns `true` if this set is a superset of [otherSet]. + /// + /// Example: + /// ```dart + /// {1, 2, 3}.isSupersetOf({1, 2}); // true + /// {1, 2}.isSupersetOf({1, 2, 3}); // false + /// ``` + bool isSupersetOf(Set otherSet) => otherSet.difference(this).isEmpty; + + /// Returns the union of this set with all [sets]. + /// + /// Example: + /// ```dart + /// {1, 2}.unionAll([{2, 3}, {3, 4}]); // {1, 2, 3, 4} + /// ``` + Set unionAll(Iterable> sets) { + var result = Set.from(this); + for (var set in sets) { + result = result.union(set); + } + return result; + } + + /// Returns a new set without [element]. + /// + /// Example: + /// ```dart + /// {1, 2, 3}.without(2); // {1, 3} + /// ``` + Set without(T element) { + return Set.from(this)..remove(element); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index cf12ba4..5d2bd31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: mayr_extensions description: A comprehensive set of handy Dart and Flutter extensions to make your code cleaner, shorter, and more expressive. -version: 0.4.0 +version: 1.0.0 homepage: https://github.com/YoungMayor/flutter_utils_extensions repository: https://github.com/YoungMayor/flutter_utils_extensions issue_tracker: https://github.com/YoungMayor/flutter_utils_extensions/issues diff --git a/test/extensions/humanize_test.dart b/test/extensions/humanize_test.dart new file mode 100644 index 0000000..f4bc069 --- /dev/null +++ b/test/extensions/humanize_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mayr_extensions/mayr_extensions.dart'; + +void main() { + group('Duration Humanize Extensions', () { + test('humanize formats duration correctly', () { + expect(Duration(hours: 2, minutes: 3).humanize(), '2 hours, 3 minutes'); + expect(Duration(days: 1).humanize(), '1 day'); + expect(Duration(seconds: 45).humanize(), '45 seconds'); + expect(Duration(hours: 1, minutes: 30).humanize(), '1 hour, 30 minutes'); + expect(Duration(days: 2, hours: 3, minutes: 15).humanize(), + '2 days, 3 hours, and 15 minutes'); + }); + + test('humanize handles zero duration', () { + expect(Duration.zero.humanize(), '0 seconds'); + }); + + test('humanize handles single units', () { + expect(Duration(days: 1).humanize(), '1 day'); + expect(Duration(hours: 1).humanize(), '1 hour'); + expect(Duration(minutes: 1).humanize(), '1 minute'); + expect(Duration(seconds: 1).humanize(), '1 second'); + }); + }); + + group('DateTime Humanize Extensions', () { + test('humanize returns "just now" for recent time', () { + final now = DateTime.now(); + expect(now.humanize(), 'just now'); + }); + + test('humanize formats past times correctly', () { + final now = DateTime.now(); + + expect(now.subtract(Duration(minutes: 5)).humanize(), '5 minutes ago'); + expect(now.subtract(Duration(hours: 3)).humanize(), '3 hours ago'); + expect(now.subtract(Duration(days: 1)).humanize(), 'yesterday'); + expect(now.subtract(Duration(days: 3)).humanize(), '3 days ago'); + expect(now.subtract(Duration(days: 10)).humanize(), 'last week'); + }); + + test('humanize formats future times correctly', () { + final now = DateTime.now(); + + expect(now.add(Duration(minutes: 5)).humanize(), 'in 5 minutes'); + expect(now.add(Duration(hours: 3)).humanize(), 'in 3 hours'); + expect(now.add(Duration(days: 1)).humanize(), 'tomorrow'); + expect(now.add(Duration(days: 3)).humanize(), 'in 3 days'); + expect(now.add(Duration(days: 10)).humanize(), 'next week'); + }); + + test('humanize handles singular values', () { + final now = DateTime.now(); + + expect(now.subtract(Duration(minutes: 1)).humanize(), '1 minute ago'); + expect(now.subtract(Duration(hours: 1)).humanize(), '1 hour ago'); + expect(now.add(Duration(minutes: 1)).humanize(), 'in 1 minute'); + expect(now.add(Duration(hours: 1)).humanize(), 'in 1 hour'); + }); + }); + + group('Num Humanize Extensions', () { + test('humanizeNumber formats numbers correctly', () { + expect(999.humanizeNumber(), '999'); + expect(1234.humanizeNumber(), '1.2k'); + expect(1500000.humanizeNumber(), '1.5M'); + expect(1000000000.humanizeNumber(), '1.0B'); + }); + + test('humanizeNumber handles decimals', () { + expect(1500.humanizeNumber(decimals: 2), '1.50k'); + expect(1234567.humanizeNumber(decimals: 0), '1M'); + }); + + test('humanizeOrdinal formats ordinals correctly', () { + expect(1.humanizeOrdinal(), '1st'); + expect(2.humanizeOrdinal(), '2nd'); + expect(3.humanizeOrdinal(), '3rd'); + expect(4.humanizeOrdinal(), '4th'); + expect(11.humanizeOrdinal(), '11th'); + expect(12.humanizeOrdinal(), '12th'); + expect(13.humanizeOrdinal(), '13th'); + expect(21.humanizeOrdinal(), '21st'); + expect(22.humanizeOrdinal(), '22nd'); + expect(23.humanizeOrdinal(), '23rd'); + expect(101.humanizeOrdinal(), '101st'); + }); + + test('humanizeCount formats counts correctly', () { + expect(0.humanizeCount('item'), '0 items'); + expect(1.humanizeCount('item'), '1 item'); + expect(3.humanizeCount('item'), '3 items'); + expect(2.humanizeCount('person', pluralName: 'people'), '2 people'); + }); + + test('humanizePercentage formats percentages correctly', () { + expect(0.75.humanizePercentage(), '75%'); + expect(0.5.humanizePercentage(), '50%'); + expect(50.humanizePercentage(max: 100), '50%'); + expect(0.333.humanizePercentage(decimals: 1), '33.3%'); + }); + + test('humanizeFileSize formats file sizes correctly', () { + expect(0.humanizeFileSize(), '0 B'); + expect(500.humanizeFileSize(), '500 B'); + expect(1024.humanizeFileSize(), '1.0 KB'); + expect(1048576.humanizeFileSize(), '1.0 MB'); + expect(520300.humanizeFileSize(), '508.1 KB'); + expect(1073741824.humanizeFileSize(), '1.0 GB'); + }); + + test('humanizeFileSize handles decimals', () { + expect(1536.humanizeFileSize(decimals: 2), '1.50 KB'); + expect(1024.humanizeFileSize(decimals: 0), '1 KB'); + }); + + test('humanizeFileSize handles invalid sizes', () { + expect((-100).humanizeFileSize(), 'Invalid size'); + }); + }); +} diff --git a/test/extensions/list_test.dart b/test/extensions/list_test.dart new file mode 100644 index 0000000..5440c84 --- /dev/null +++ b/test/extensions/list_test.dart @@ -0,0 +1,242 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mayr_extensions/mayr_extensions.dart'; + +void main() { + group('Iterable Extensions', () { + test('firstOrNull returns first element or null', () { + expect([1, 2, 3].firstOrNull(), 1); + expect([].firstOrNull(), null); + }); + + test('lastOrNull returns last element or null', () { + expect([1, 2, 3].lastOrNull(), 3); + expect([].lastOrNull(), null); + }); + + test('singleWhereOrNull returns matching element or null', () { + expect([1, 2, 3].singleWhereOrNull((e) => e == 2), 2); + expect([1, 2, 3].singleWhereOrNull((e) => e > 5), null); + expect([1, 2, 2, 3].singleWhereOrNull((e) => e == 2), null); // Multiple matches + }); + + test('containsWhere checks if predicate is satisfied', () { + expect([1, 2, 3].containsWhere((e) => e > 2), true); + expect([1, 2, 3].containsWhere((e) => e > 5), false); + }); + + test('indexWhereOrNull returns index or null', () { + expect([1, 2, 3].indexWhereOrNull((e) => e == 2), 1); + expect([1, 2, 3].indexWhereOrNull((e) => e > 5), null); + }); + + test('mapIndexed maps with index', () { + final result = ['a', 'b', 'c'].mapIndexed((i, e) => '$i: $e').toList(); + expect(result, ['0: a', '1: b', '2: c']); + }); + + test('whereNotNull filters out nulls', () { + expect([1, null, 2, null, 3].whereNotNull().toList(), [1, 2, 3]); + }); + + test('distinctBy returns unique elements by key', () { + final list = [ + {'name': 'Alice', 'age': 30}, + {'name': 'Bob', 'age': 25}, + {'name': 'Alice', 'age': 35}, + ]; + final result = list.distinctBy((e) => e['name']).toList(); + expect(result.length, 2); + expect(result[0]['name'], 'Alice'); + expect(result[1]['name'], 'Bob'); + }); + + test('sortedBy sorts by key selector', () { + final list = [ + {'name': 'Charlie', 'age': 30}, + {'name': 'Alice', 'age': 25}, + {'name': 'Bob', 'age': 35}, + ]; + final result = list.sortedBy((e) => e['name'] as String); + expect(result[0]['name'], 'Alice'); + expect(result[1]['name'], 'Bob'); + expect(result[2]['name'], 'Charlie'); + }); + + test('sortedByDesc sorts by key selector descending', () { + final list = [ + {'name': 'Charlie', 'age': 30}, + {'name': 'Alice', 'age': 25}, + {'name': 'Bob', 'age': 35}, + ]; + final result = list.sortedByDesc((e) => e['age'] as int); + expect(result[0]['age'], 35); + expect(result[1]['age'], 30); + expect(result[2]['age'], 25); + }); + + test('countWhere counts matching elements', () { + expect([1, 2, 3, 4, 5].countWhere((e) => e > 3), 2); + expect([1, 2, 3].countWhere((e) => e > 5), 0); + }); + + test('isNullOrEmpty checks if empty', () { + expect([].isNullOrEmpty(), true); + expect([1, 2, 3].isNullOrEmpty(), false); + }); + + test('joinToString joins elements', () { + expect([1, 2, 3].joinToString(separator: ', '), '1, 2, 3'); + expect( + [1, 2, 3].joinToString(separator: ', ', transform: (e) => 'Item $e'), + 'Item 1, Item 2, Item 3', + ); + }); + + test('forEachIndexed executes with index', () { + final indices = []; + final items = []; + ['a', 'b', 'c'].forEachIndexed((i, e) { + indices.add(i); + items.add(e); + }); + expect(indices, [0, 1, 2]); + expect(items, ['a', 'b', 'c']); + }); + }); + + group('List Extensions', () { + test('getOrNull returns element or null', () { + expect([1, 2, 3].getOrNull(1), 2); + expect([1, 2, 3].getOrNull(5), null); + expect([1, 2, 3].getOrNull(-1), null); + }); + + test('getOrDefault returns element or default', () { + expect([1, 2, 3].getOrDefault(1, 0), 2); + expect([1, 2, 3].getOrDefault(5, 0), 0); + }); + + test('chunked splits into chunks', () { + expect([1, 2, 3, 4, 5].chunked(2), [[1, 2], [3, 4], [5]]); + expect([1, 2, 3].chunked(1), [[1], [2], [3]]); + expect([1, 2, 3].chunked(5), [[1, 2, 3]]); + }); + + test('flatten flattens nested lists', () { + expect([[1, 2], [3, 4], [5]].flatten(), [1, 2, 3, 4, 5]); + expect([[], [1], [2, 3]].flatten(), [1, 2, 3]); + }); + + test('flip reverses the list', () { + expect([1, 2, 3].flip(), [3, 2, 1]); + expect([1].flip(), [1]); + expect([].flip(), []); + }); + + test('insertIf inserts conditionally', () { + expect([1, 2, 3].insertIf(true, 4), [1, 2, 3, 4]); + expect([1, 2, 3].insertIf(false, 4), [1, 2, 3]); + }); + + test('replaceWhere replaces matching elements', () { + expect([1, 2, 3, 2].replaceWhere((e) => e == 2, 5), [1, 5, 3, 5]); + }); + + test('removeWhereNot keeps matching elements', () { + expect([1, 2, 3, 4, 5].removeWhereNot((e) => e > 2), [3, 4, 5]); + }); + + test('updateWhere updates matching elements', () { + expect([1, 2, 3, 4].updateWhere((e) => e > 2, (e) => e * 2), [1, 2, 6, 8]); + }); + + test('addIf adds value conditionally', () { + expect([1, 2, 3].addIf(4), [1, 2, 3, 4]); + expect([1, 2, 3].addIf(null), [1, 2, 3]); + }); + + test('addAllIf adds values conditionally', () { + expect([1, 2].addAllIf([3, 4]), [1, 2, 3, 4]); + expect([1, 2].addAllIf(null), [1, 2]); + }); + + test('append adds value', () { + expect([1, 2, 3].append(4), [1, 2, 3, 4]); + }); + + test('appendAll adds all values', () { + expect([1, 2].appendAll([3, 4]), [1, 2, 3, 4]); + }); + + test('appendIf adds value conditionally', () { + expect([1, 2, 3].appendIf(4, true), [1, 2, 3, 4]); + expect([1, 2, 3].appendIf(4, false), [1, 2, 3]); + }); + + test('appendAllIf adds values conditionally', () { + expect([1, 2].appendAllIf([3, 4], true), [1, 2, 3, 4]); + expect([1, 2].appendAllIf([3, 4], false), [1, 2]); + }); + + test('pop removes and returns last element', () { + var list = [1, 2, 3]; + expect(list.pop(), 3); + expect(list, [1, 2]); + expect([].pop(), null); + }); + + test('fliter filters elements', () { + expect([1, 2, 3, 4, 5].fliter((e) => e > 2), [3, 4, 5]); + }); + + test('unique returns unique elements', () { + expect([1, 2, 2, 3, 3, 4].unique(), [1, 2, 3, 4]); + }); + }); + + group('Num Iterable Extensions', () { + test('sumBy sums elements', () { + expect([1, 2, 3, 4, 5].sumBy((e) => e), 15); + expect([2, 4, 6].sumBy((e) => e * 2), 24); + }); + + test('averageBy calculates average', () { + expect([1, 2, 3, 4, 5].averageBy((e) => e), 3.0); + expect([].averageBy((e) => e), 0); + }); + + test('min returns minimum', () { + expect([3, 1, 4, 1, 5].min(), 1); + expect([].min(), null); + }); + + test('max returns maximum', () { + expect([3, 1, 4, 1, 5].max(), 5); + expect([].max(), null); + }); + }); + + group('Comparable Iterable Extensions', () { + test('minBy returns element with minimum key', () { + final list = [ + {'name': 'Alice', 'age': 30}, + {'name': 'Bob', 'age': 25}, + {'name': 'Charlie', 'age': 35}, + ]; + final result = list.minBy((e) => e['age'] as int); + expect(result?['name'], 'Bob'); + expect([].minBy((e) => e), null); + }); + + test('maxBy returns element with maximum key', () { + final list = [ + {'name': 'Alice', 'age': 30}, + {'name': 'Bob', 'age': 25}, + {'name': 'Charlie', 'age': 35}, + ]; + final result = list.maxBy((e) => e['age'] as int); + expect(result?['name'], 'Charlie'); + expect([].maxBy((e) => e), null); + }); + }); +} diff --git a/test/extensions/map_test.dart b/test/extensions/map_test.dart new file mode 100644 index 0000000..b4a7dcb --- /dev/null +++ b/test/extensions/map_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mayr_extensions/mayr_extensions.dart'; + +void main() { + group('Map Extensions', () { + test('getOrNull returns value or null', () { + expect({'a': 1, 'b': 2}.getOrNull('a'), 1); + expect({'a': 1, 'b': 2}.getOrNull('c'), null); + }); + + test('getOrDefault returns value or default', () { + expect({'a': 1, 'b': 2}.getOrDefault('a', 0), 1); + expect({'a': 1, 'b': 2}.getOrDefault('c', 0), 0); + }); + + test('mapKeys transforms keys', () { + final result = {'a': 1, 'b': 2}.mapKeys((k, v) => k.toUpperCase()); + expect(result, {'A': 1, 'B': 2}); + }); + + test('mapValues transforms values', () { + final result = {'a': 1, 'b': 2}.mapValues((k, v) => v * 2); + expect(result, {'a': 2, 'b': 4}); + }); + + test('filterKeys filters by key predicate', () { + final result = {'a': 1, 'b': 2, 'c': 3}.filterKeys((k) => k != 'b'); + expect(result, {'a': 1, 'c': 3}); + }); + + test('filterValues filters by value predicate', () { + final result = {'a': 1, 'b': 2, 'c': 3}.filterValues((v) => v > 1); + expect(result, {'b': 2, 'c': 3}); + }); + + test('invert swaps keys and values', () { + final result = {'a': 1, 'b': 2}.invert(); + expect(result, {1: 'a', 2: 'b'}); + }); + + test('merge combines maps with precedence', () { + final result = {'a': 1, 'b': 2}.merge({'b': 3, 'c': 4}); + expect(result, {'a': 1, 'b': 3, 'c': 4}); + }); + + test('mergeIfAbsent combines maps without overriding', () { + final result = {'a': 1, 'b': 2}.mergeIfAbsent({'b': 3, 'c': 4}); + expect(result, {'a': 1, 'b': 2, 'c': 4}); + }); + + test('combine merges with custom combiner', () { + final result = {'a': 1, 'b': 2}.combine( + {'b': 3, 'c': 4}, + (k, v1, v2) => v1 + v2, + ); + expect(result, {'a': 1, 'b': 5, 'c': 4}); + }); + + test('keysWhere returns keys matching predicate', () { + final result = {'a': 1, 'b': 2, 'c': 3}.keysWhere((v) => v > 1).toList(); + expect(result, ['b', 'c']); + }); + + test('valuesWhere returns values matching predicate', () { + final result = {'a': 1, 'b': 2, 'c': 3}.valuesWhere((k) => k != 'b').toList(); + expect(result, [1, 3]); + }); + + test('toQueryString converts to URL query string', () { + final result = {'name': 'John', 'age': '30'}.toQueryString(); + expect(result, 'name=John&age=30'); + }); + + test('toQueryString handles special characters', () { + final result = {'key': 'value with spaces', 'special': 'a&b=c'}.toQueryString(); + expect(result.contains('value+with+spaces') || result.contains('value%20with%20spaces'), true); + }); + }); +} diff --git a/test/extensions/set_test.dart b/test/extensions/set_test.dart new file mode 100644 index 0000000..69e5635 --- /dev/null +++ b/test/extensions/set_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mayr_extensions/mayr_extensions.dart'; + +void main() { + group('Set Extensions', () { + test('toggle adds element if missing', () { + final result = {1, 2, 3}.toggle(4); + expect(result, {1, 2, 3, 4}); + }); + + test('toggle removes element if present', () { + final result = {1, 2, 3}.toggle(2); + expect(result, {1, 3}); + }); + + test('intersects returns true if sets have common elements', () { + expect({1, 2, 3}.intersects({2, 3, 4}), true); + expect({1, 2}.intersects({3, 4}), false); + }); + + test('isSubsetOf returns true if this set is contained in other', () { + expect({1, 2}.isSubsetOf({1, 2, 3}), true); + expect({1, 4}.isSubsetOf({1, 2, 3}), false); + expect({1, 2, 3}.isSubsetOf({1, 2, 3}), true); + }); + + test('isSupersetOf returns true if this set contains other', () { + expect({1, 2, 3}.isSupersetOf({1, 2}), true); + expect({1, 2}.isSupersetOf({1, 2, 3}), false); + expect({1, 2, 3}.isSupersetOf({1, 2, 3}), true); + }); + + test('unionAll combines multiple sets', () { + final result = {1, 2}.unionAll([{2, 3}, {3, 4}]); + expect(result, {1, 2, 3, 4}); + }); + + test('without removes element', () { + final result = {1, 2, 3}.without(2); + expect(result, {1, 3}); + }); + + test('without does nothing if element not present', () { + final result = {1, 2, 3}.without(4); + expect(result, {1, 2, 3}); + }); + }); +} From d95996e19beb348d474cc1c9e9f91bba0c5bac4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:55:26 +0000 Subject: [PATCH 3/6] Update CHANGELOG and README with new v1.0.0 features Co-authored-by: YoungMayor <47315212+YoungMayor@users.noreply.github.com> --- CHANGELOG.md | 101 ++++++++++++++++++++++ README.md | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23341ea..f336828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,107 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0] - Major Release with New Extensions + +### Added + +#### Iterable/List Extensions +- **Query & Search:** + - `firstOrNull()` - Returns first element or `null` if empty + - `lastOrNull()` - Returns last element or `null` if empty + - `singleWhereOrNull(predicate)` - Returns match or `null` + - `containsWhere(predicate)` - Boolean check + - `indexWhereOrNull(predicate)` - Returns index or `null` + +- **Safe Access:** + - `getOrNull(index)` - Returns element at index or `null` + - `getOrDefault(index, defaultValue)` - Returns element or default value + +- **Transformations:** + - `chunked(size)` - Splits into chunks + - `mapIndexed((index, item) => ...)` - Maps with index + - `whereNotNull()` - Filters out nulls + - `distinctBy(keySelector)` - Unique items by property + - `flatten()` - Flattens nested lists + - `sortedBy(keySelector)` / `sortedByDesc(keySelector)` - Sort by property + - `flip()` - Reverses the list + +- **Aggregations (for numbers):** + - `sumBy(num Function(T))` - Sum elements by selector + - `averageBy(num Function(T))` - Average by selector + - `minBy(keySelector)` / `min()` - Minimum value/element + - `maxBy(keySelector)` / `max()` - Maximum value/element + - `countWhere(predicate)` - Count matching elements + +- **Mutation Helpers (returns new copy):** + - `insertIf(condition, value)` - Insert conditionally + - `replaceWhere(predicate, newValue)` - Replace matching elements + - `removeWhereNot(predicate)` - Keep only matching elements + - `updateWhere(predicate, updater)` - Update matching elements + - `addIf(value)` / `addAllIf(values)` - Add conditionally + - `append(value)` / `appendAll(values)` - Append elements + - `appendIf(value)` / `appendAllIf(values)` - Append conditionally + - `pop()` - Remove and return last element + - `fliter(predicate)` - Filter elements (alias for where) + - `unique()` - Get unique elements + +- **Utility:** + - `isNullOrEmpty()` - Check if empty + - `joinToString(separator, transform)` - Join with custom format + - `forEachIndexed()` - Iterate with index + +#### Map Extensions +- **Safe Access:** + - `getOrNull(key)` - Get value or null + - `getOrDefault(key, defaultValue)` - Get value or default + +- **Transformations:** + - `mapKeys((k, v) => newKey)` - Transform keys + - `mapValues((k, v) => newValue)` - Transform values + - `filterKeys(predicate)` - Filter by keys + - `filterValues(predicate)` - Filter by values + - `invert()` - Swap keys and values + +- **Merge & Combine:** + - `merge(otherMap)` - Merge with precedence + - `mergeIfAbsent(otherMap)` - Merge without overriding + - `combine(other, (k, v1, v2) => mergedValue)` - Custom merge + +- **Utility:** + - `keysWhere(predicate)` - Get keys matching predicate + - `valuesWhere(predicate)` - Get values matching predicate + - `toQueryString()` - Convert to URL query string + +#### Set Extensions +- `toggle(element)` - Add if missing, remove if present +- `intersects(otherSet)` - Check for intersection +- `isSubsetOf(otherSet)` - Check if subset +- `isSupersetOf(otherSet)` - Check if superset +- `unionAll(sets)` - Union of multiple sets +- `without(element)` - Remove element + +#### Humanize Extensions +- **Duration:** + - `humanize(locale)` - Convert to "2 hours, 3 minutes" format + +- **DateTime:** + - `humanize(locale)` - Convert to relative time ("just now", "3 hours ago", "yesterday", "last week", etc.) + +- **Numbers:** + - `humanizeNumber()` - Format as "15.3k", "1.5M", etc. + - `humanizeOrdinal()` - Format as "1st", "2nd", "3rd", etc. + - `humanizeCount('item')` - Format as "1 item" / "3 items" + - `humanizePercentage(max, min)` - Format as "74%" + - `humanizeFileSize()` - Format as "1.0 MB", "520.3 KB", etc. + +### Changed +- Updated package version to 1.0.0 marking stable release + +### Improved +- Added comprehensive test coverage for all new extensions +- Complete documentation with examples for all methods +- Enhanced type safety with proper generic constraints + ## [Unreleased] ### Improved diff --git a/README.md b/README.md index 20a84cc..6c867d3 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,238 @@ await 2.seconds.delay(); // Waits for 2 seconds - `isNumericOnly` +------------------------------------------------------------------------------- + +### 🧩 Iterable / List Extensions + +#### Query & Search + +- `firstOrNull()` β†’ Returns first element or `null` if empty +- `lastOrNull()` β†’ Returns last element or `null` if empty +- `singleWhereOrNull(predicate)` β†’ Returns match or `null` +- `containsWhere(predicate)` β†’ Boolean check +- `indexWhereOrNull(predicate)` β†’ Returns index or `null` + +```dart +// Examples +[1, 2, 3].firstOrNull(); // 1 +[].firstOrNull(); // null +[1, 2, 3].singleWhereOrNull((e) => e == 2); // 2 +[1, 2, 3].containsWhere((e) => e > 2); // true +``` + +#### Safe Access + +- `getOrNull(index)` β†’ Returns element at index or `null` +- `getOrDefault(index, defaultValue)` β†’ Returns element or default value + +```dart +// Examples +[1, 2, 3].getOrNull(1); // 2 +[1, 2, 3].getOrNull(5); // null +[1, 2, 3].getOrDefault(5, 0); // 0 +``` + +#### Transformations + +- `chunked(size)` β†’ Splits into chunks +- `mapIndexed((index, item) => ...)` β†’ Maps with index +- `whereNotNull()` β†’ Filters out nulls +- `distinctBy(keySelector)` β†’ Unique items by property +- `flatten()` β†’ Flattens nested lists +- `sortedBy(keySelector)` / `sortedByDesc(keySelector)` β†’ Sort by property +- `flip()` β†’ Reverses the list + +```dart +// Examples +[1, 2, 3, 4, 5].chunked(2); // [[1, 2], [3, 4], [5]] +['a', 'b', 'c'].mapIndexed((i, e) => '$i: $e'); // ['0: a', '1: b', '2: c'] +[1, null, 2, null, 3].whereNotNull(); // [1, 2, 3] +[[1, 2], [3, 4]].flatten(); // [1, 2, 3, 4] +[1, 2, 3].flip(); // [3, 2, 1] +``` + +#### Aggregations (Only available on list of numbers) + +- `sumBy(num Function(T))` β†’ Sum elements by selector +- `averageBy(num Function(T))` β†’ Average by selector +- `minBy(keySelector)` β†’ Element with minimum key +- `min()` β†’ Minimum value +- `maxBy(keySelector)` β†’ Element with maximum key +- `max()` β†’ Maximum value +- `countWhere(predicate)` β†’ Count matching elements + +```dart +// Examples +[1, 2, 3, 4, 5].sumBy((e) => e); // 15 +[1, 2, 3, 4, 5].averageBy((e) => e); // 3.0 +[3, 1, 4, 1, 5].min(); // 1 +[3, 1, 4, 1, 5].max(); // 5 +[1, 2, 3, 4, 5].countWhere((e) => e > 3); // 2 +``` + +#### Mutation Helpers (returns new copy) + +- `insertIf(condition, value)` β†’ Insert conditionally +- `replaceWhere(predicate, newValue)` β†’ Replace matching elements +- `removeWhereNot(predicate)` β†’ Keep only matching elements +- `updateWhere(predicate, updater)` β†’ Update matching elements +- `addIf(value)` / `addAllIf(values)` β†’ Add conditionally +- `append(value)` / `appendAll(values)` β†’ Append elements +- `appendIf(value)` / `appendAllIf(values)` β†’ Append conditionally +- `pop()` β†’ Remove and return last element +- `fliter(predicate)` β†’ Filter elements +- `unique()` β†’ Get unique elements + +```dart +// Examples +[1, 2, 3].insertIf(true, 4); // [1, 2, 3, 4] +[1, 2, 3, 2].replaceWhere((e) => e == 2, 5); // [1, 5, 3, 5] +[1, 2, 3, 4, 5].removeWhereNot((e) => e > 2); // [3, 4, 5] +[1, 2, 2, 3, 3, 4].unique(); // [1, 2, 3, 4] +``` + +#### Utility + +- `isNullOrEmpty()` β†’ Check if empty +- `joinToString(separator, transform)` β†’ Join with custom format +- `forEachIndexed()` β†’ Iterate with index + +```dart +// Examples +[].isNullOrEmpty(); // true +[1, 2, 3].joinToString(separator: ', '); // '1, 2, 3' +['a', 'b'].forEachIndexed((i, e) => print('$i: $e')); +``` + +------------------------------------------------------------------------------- + +### πŸ—ΊοΈ Map Extensions + +#### Safe Access + +- `getOrNull(key)` β†’ Get value or null +- `getOrDefault(key, defaultValue)` β†’ Get value or default + +```dart +// Examples +{'a': 1, 'b': 2}.getOrNull('a'); // 1 +{'a': 1, 'b': 2}.getOrNull('c'); // null +{'a': 1, 'b': 2}.getOrDefault('c', 0); // 0 +``` + +#### Transformations + +- `mapKeys((k, v) => newKey)` β†’ Transform keys +- `mapValues((k, v) => newValue)` β†’ Transform values +- `filterKeys(predicate)` β†’ Filter by keys +- `filterValues(predicate)` β†’ Filter by values +- `invert()` β†’ Swap keys and values + +```dart +// Examples +{'a': 1, 'b': 2}.mapKeys((k, v) => k.toUpperCase()); // {'A': 1, 'B': 2} +{'a': 1, 'b': 2}.mapValues((k, v) => v * 2); // {'a': 2, 'b': 4} +{'a': 1, 'b': 2}.invert(); // {1: 'a', 2: 'b'} +``` + +#### Merge & Combine + +- `merge(otherMap)` β†’ Merge with precedence +- `mergeIfAbsent(otherMap)` β†’ Merge without overriding +- `combine(other, (k, v1, v2) => mergedValue)` β†’ Custom merge + +```dart +// Examples +{'a': 1, 'b': 2}.merge({'b': 3, 'c': 4}); // {'a': 1, 'b': 3, 'c': 4} +{'a': 1, 'b': 2}.mergeIfAbsent({'b': 3, 'c': 4}); // {'a': 1, 'b': 2, 'c': 4} +``` + +#### Utility + +- `keysWhere(predicate)` β†’ Get keys matching predicate +- `valuesWhere(predicate)` β†’ Get values matching predicate +- `toQueryString()` β†’ Convert to URL query string + +```dart +// Examples +{'a': 1, 'b': 2, 'c': 3}.keysWhere((v) => v > 1); // ['b', 'c'] +{'name': 'John', 'age': '30'}.toQueryString(); // 'name=John&age=30' +``` + +------------------------------------------------------------------------------- + +### πŸ”’ Set Extensions + +- `toggle(element)` β†’ Adds if missing, removes if present +- `intersects(otherSet)` β†’ Check for intersection +- `isSubsetOf(otherSet)` β†’ Check if subset +- `isSupersetOf(otherSet)` β†’ Check if superset +- `unionAll(sets)` β†’ Union of multiple sets +- `without(element)` β†’ Remove element + +```dart +// Examples +{1, 2, 3}.toggle(2); // {1, 3} +{1, 2, 3}.toggle(4); // {1, 2, 3, 4} +{1, 2, 3}.intersects({2, 3, 4}); // true +{1, 2}.isSubsetOf({1, 2, 3}); // true +{1, 2}.unionAll([{2, 3}, {3, 4}]); // {1, 2, 3, 4} +``` + +------------------------------------------------------------------------------- + +### 🎯 Humanize Extensions + +The goal of `humanize` is simple: + +> Convert technical or numeric values into **readable, natural, human-friendly strings**. + +Where computers speak in seconds, bytes, and counts, `humanize` translates them into something that sounds like it came from a person. + +#### Durations + +- `.humanize(locale)` β†’ "2 hours, 3 minutes" + +```dart +// Examples +Duration(hours: 2, minutes: 3).humanize(); // '2 hours, 3 minutes' +Duration(days: 1).humanize(); // '1 day' +Duration(seconds: 45).humanize(); // '45 seconds' +``` + +#### Time (DateTime) + +- `.humanize(locale)` β†’ "just now", "3 hours ago", "yesterday", "last week", "3 days from now", "2 weeks ago" + +```dart +// Examples +DateTime.now().humanize(); // 'just now' +DateTime.now().subtract(Duration(hours: 3)).humanize(); // '3 hours ago' +DateTime.now().subtract(Duration(days: 1)).humanize(); // 'yesterday' +DateTime.now().add(Duration(days: 2)).humanize(); // 'in 2 days' +``` + +#### Numbers + +- `humanizeNumber()` β†’ "15.3k", "1.5M" +- `humanizeOrdinal()` β†’ "1st", "2nd", "3rd" +- `humanizeCount('item')` β†’ "1 item" / "3 items" +- `humanizePercentage(max, min)` β†’ "74%" +- `humanizeFileSize()` β†’ "1.0 MB", "520.3 KB" + +```dart +// Examples +1234.humanizeNumber(); // '1.2k' +1500000.humanizeNumber(); // '1.5M' +1.humanizeOrdinal(); // '1st' +21.humanizeOrdinal(); // '21st' +3.humanizeCount('item'); // '3 items' +0.75.humanizePercentage(); // '75%' +1024.humanizeFileSize(); // '1.0 KB' +520300.humanizeFileSize(); // '508.1 KB' +``` + ------------------------------------------------------------------------------- ### 🧩 Widget Extensions From 065695040dc2fe8de67e754fedebf525a6c10aba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:49:37 +0000 Subject: [PATCH 4/6] Fix flaky DateTime humanize test by adding tolerance for timing differences Co-authored-by: YoungMayor <47315212+YoungMayor@users.noreply.github.com> --- test/extensions/humanize_test.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/extensions/humanize_test.dart b/test/extensions/humanize_test.dart index f4bc069..5f0691c 100644 --- a/test/extensions/humanize_test.dart +++ b/test/extensions/humanize_test.dart @@ -33,7 +33,11 @@ void main() { test('humanize formats past times correctly', () { final now = DateTime.now(); - expect(now.subtract(Duration(minutes: 5)).humanize(), '5 minutes ago'); + // Use a time far enough in the past to avoid timing issues + final fiveMinutesAgo = now.subtract(Duration(minutes: 5, seconds: 30)); + final result = fiveMinutesAgo.humanize(); + expect(result == '5 minutes ago' || result == '6 minutes ago', true); + expect(now.subtract(Duration(hours: 3)).humanize(), '3 hours ago'); expect(now.subtract(Duration(days: 1)).humanize(), 'yesterday'); expect(now.subtract(Duration(days: 3)).humanize(), '3 days ago'); @@ -43,7 +47,11 @@ void main() { test('humanize formats future times correctly', () { final now = DateTime.now(); - expect(now.add(Duration(minutes: 5)).humanize(), 'in 5 minutes'); + // Use a time far enough in the future to avoid timing issues + final fiveMinutesLater = now.add(Duration(minutes: 5, seconds: 30)); + final result = fiveMinutesLater.humanize(); + expect(result == 'in 5 minutes' || result == 'in 6 minutes', true); + expect(now.add(Duration(hours: 3)).humanize(), 'in 3 hours'); expect(now.add(Duration(days: 1)).humanize(), 'tomorrow'); expect(now.add(Duration(days: 3)).humanize(), 'in 3 days'); From ac15e0f00c4dadb0aa1a1f6676132be613cd3164 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:56:48 +0000 Subject: [PATCH 5/6] Fix additional timing issues in humanize tests for hours and singular values Co-authored-by: YoungMayor <47315212+YoungMayor@users.noreply.github.com> --- test/extensions/humanize_test.dart | 32 ++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/test/extensions/humanize_test.dart b/test/extensions/humanize_test.dart index 5f0691c..6714c54 100644 --- a/test/extensions/humanize_test.dart +++ b/test/extensions/humanize_test.dart @@ -38,7 +38,11 @@ void main() { final result = fiveMinutesAgo.humanize(); expect(result == '5 minutes ago' || result == '6 minutes ago', true); - expect(now.subtract(Duration(hours: 3)).humanize(), '3 hours ago'); + // Add extra seconds to avoid boundary timing issues + final threeHoursAgo = now.subtract(Duration(hours: 3, seconds: 30)); + final hoursResult = threeHoursAgo.humanize(); + expect(hoursResult == '3 hours ago' || hoursResult == '4 hours ago', true); + expect(now.subtract(Duration(days: 1)).humanize(), 'yesterday'); expect(now.subtract(Duration(days: 3)).humanize(), '3 days ago'); expect(now.subtract(Duration(days: 10)).humanize(), 'last week'); @@ -52,7 +56,11 @@ void main() { final result = fiveMinutesLater.humanize(); expect(result == 'in 5 minutes' || result == 'in 6 minutes', true); - expect(now.add(Duration(hours: 3)).humanize(), 'in 3 hours'); + // Add extra seconds to avoid boundary timing issues + final threeHoursLater = now.add(Duration(hours: 3, seconds: 30)); + final hoursResult = threeHoursLater.humanize(); + expect(hoursResult == 'in 3 hours' || hoursResult == 'in 4 hours', true); + expect(now.add(Duration(days: 1)).humanize(), 'tomorrow'); expect(now.add(Duration(days: 3)).humanize(), 'in 3 days'); expect(now.add(Duration(days: 10)).humanize(), 'next week'); @@ -61,10 +69,22 @@ void main() { test('humanize handles singular values', () { final now = DateTime.now(); - expect(now.subtract(Duration(minutes: 1)).humanize(), '1 minute ago'); - expect(now.subtract(Duration(hours: 1)).humanize(), '1 hour ago'); - expect(now.add(Duration(minutes: 1)).humanize(), 'in 1 minute'); - expect(now.add(Duration(hours: 1)).humanize(), 'in 1 hour'); + // Add buffer seconds to avoid timing boundary issues + final oneMinuteAgo = now.subtract(Duration(minutes: 1, seconds: 30)); + final minuteAgoResult = oneMinuteAgo.humanize(); + expect(minuteAgoResult == '1 minute ago' || minuteAgoResult == '2 minutes ago', true); + + final oneHourAgo = now.subtract(Duration(hours: 1, seconds: 30)); + final hourAgoResult = oneHourAgo.humanize(); + expect(hourAgoResult == '1 hour ago' || hourAgoResult == '2 hours ago', true); + + final oneMinuteLater = now.add(Duration(minutes: 1, seconds: 30)); + final minuteLaterResult = oneMinuteLater.humanize(); + expect(minuteLaterResult == 'in 1 minute' || minuteLaterResult == 'in 2 minutes', true); + + final oneHourLater = now.add(Duration(hours: 1, seconds: 30)); + final hourLaterResult = oneHourLater.humanize(); + expect(hourLaterResult == 'in 1 hour' || hourLaterResult == 'in 2 hours', true); }); }); From e960b1ddadb06fd11d430975d77341869ffe456d Mon Sep 17 00:00:00 2001 From: YoungMayor Date: Tue, 7 Oct 2025 23:39:36 +0100 Subject: [PATCH 6/6] Remove minBy and maxBy extensions from Iterable The minBy and maxBy extensions for Iterable have been removed from the codebase, documentation, and tests. This simplifies the API and focuses on min and max for numeric iterables. Related documentation and test cases have been updated accordingly. --- CHANGELOG.md | 4 +- README.md | 2 - lib/src/extensions/humanize.dart | 10 ++-- lib/src/extensions/list.dart | 40 ++-------------- lib/src/extensions/map.dart | 17 +++---- test/extensions/humanize_test.dart | 69 ++++++++++++++++++---------- test/extensions/list_test.dart | 74 ++++++++++++++++-------------- test/extensions/map_test.dart | 20 +++++--- test/extensions/set_test.dart | 5 +- 9 files changed, 120 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f336828..44cd95d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,8 +33,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Aggregations (for numbers):** - `sumBy(num Function(T))` - Sum elements by selector - `averageBy(num Function(T))` - Average by selector - - `minBy(keySelector)` / `min()` - Minimum value/element - - `maxBy(keySelector)` / `max()` - Maximum value/element - `countWhere(predicate)` - Count matching elements - **Mutation Helpers (returns new copy):** @@ -127,7 +125,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `truncate(maxLength)` - Truncates with word boundary awareness - `wrap(prefix, suffix)` - Wraps string with prefix and suffix - `removePrefix(prefix)` and `removeSuffix(suffix)` - Remove specific prefix or suffix - + - **Number Extensions:** - `toDecimalPlaces(places)` - Rounds double to specified decimal places - `isBetween(min, max)` - Checks if number is within range diff --git a/README.md b/README.md index 6c867d3..467ac08 100644 --- a/README.md +++ b/README.md @@ -422,9 +422,7 @@ await 2.seconds.delay(); // Waits for 2 seconds - `sumBy(num Function(T))` β†’ Sum elements by selector - `averageBy(num Function(T))` β†’ Average by selector -- `minBy(keySelector)` β†’ Element with minimum key - `min()` β†’ Minimum value -- `maxBy(keySelector)` β†’ Element with maximum key - `max()` β†’ Maximum value - `countWhere(predicate)` β†’ Count matching elements diff --git a/lib/src/extensions/humanize.dart b/lib/src/extensions/humanize.dart index c4ccb2f..41e3644 100644 --- a/lib/src/extensions/humanize.dart +++ b/lib/src/extensions/humanize.dart @@ -16,7 +16,7 @@ extension MayrDurationHumanizeExtensions on Duration { final seconds = inSeconds.remainder(60); final parts = []; - + if (days > 0) { parts.add('$days ${days == 1 ? 'day' : 'days'}'); } @@ -33,8 +33,8 @@ extension MayrDurationHumanizeExtensions on Duration { if (parts.isEmpty) return '0 seconds'; if (parts.length == 1) return parts[0]; if (parts.length == 2) return '${parts[0]}, ${parts[1]}'; - - return parts.sublist(0, parts.length - 1).join(', ') + ', and ${parts.last}'; + + return '${parts.sublist(0, parts.length - 1).join(', ')}, and ${parts.last}'; } } @@ -154,7 +154,7 @@ extension MayrNumHumanizeExtensions on num { if (n % 100 >= 11 && n % 100 <= 13) { return '${n}th'; } - + switch (n % 10) { case 1: return '${n}st'; @@ -207,7 +207,7 @@ extension MayrNumHumanizeExtensions on num { String humanizeFileSize({int decimals = 1}) { if (this < 0) return 'Invalid size'; if (this == 0) return '0 B'; - + const suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; var index = 0; var size = toDouble(); diff --git a/lib/src/extensions/list.dart b/lib/src/extensions/list.dart index adaa74c..bc87c3c 100644 --- a/lib/src/extensions/list.dart +++ b/lib/src/extensions/list.dart @@ -129,8 +129,7 @@ extension MayrIterableExtensions on Iterable { /// ```dart /// [1, 2, 3, 4, 5].countWhere((e) => e > 3); // 2 /// ``` - int countWhere(bool Function(T) predicate) => - where(predicate).length; + int countWhere(bool Function(T) predicate) => where(predicate).length; /// Checks if the iterable is `null` or empty. /// @@ -149,10 +148,7 @@ extension MayrIterableExtensions on Iterable { /// [1, 2, 3].joinToString(separator: ', ', transform: (e) => 'Item $e'); /// // 'Item 1, Item 2, Item 3' /// ``` - String joinToString({ - String separator = '', - String Function(T)? transform, - }) { + String joinToString({String separator = '', String Function(T)? transform}) { if (transform != null) { return map(transform).join(separator); } @@ -386,7 +382,7 @@ extension MayrListExtensions on List { List unique() => toSet().toList(); } -extension MayrNumIterableExtensions on Iterable { +extension MayrNumIterableExtensions on Iterable { /// Sums elements by applying a selector function. /// /// Example: @@ -414,7 +410,7 @@ extension MayrNumIterableExtensions on Iterable { /// [3, 1, 4, 1, 5].min(); // 1 /// [].min(); // null /// ``` - num? min() => isEmpty ? null : reduce((a, b) => a < b ? a : b); + T? min() => isEmpty ? null : reduce((a, b) => a < b ? a : b); /// Returns the maximum value in the iterable. /// @@ -423,31 +419,5 @@ extension MayrNumIterableExtensions on Iterable { /// [3, 1, 4, 1, 5].max(); // 5 /// [].max(); // null /// ``` - num? max() => isEmpty ? null : reduce((a, b) => a > b ? a : b); -} - -extension MayrComparableIterableExtensions on Iterable { - /// Returns the minimum element by comparing with a key selector. - /// - /// Example: - /// ```dart - /// [Person('Alice', 30), Person('Bob', 25)].minBy((p) => p.age); - /// // Person('Bob', 25) - /// ``` - T? minBy>(K Function(T) keySelector) { - if (isEmpty) return null; - return reduce((a, b) => keySelector(a).compareTo(keySelector(b)) < 0 ? a : b); - } - - /// Returns the maximum element by comparing with a key selector. - /// - /// Example: - /// ```dart - /// [Person('Alice', 30), Person('Bob', 25)].maxBy((p) => p.age); - /// // Person('Alice', 30) - /// ``` - T? maxBy>(K Function(T) keySelector) { - if (isEmpty) return null; - return reduce((a, b) => keySelector(a).compareTo(keySelector(b)) > 0 ? a : b); - } + T? max() => isEmpty ? null : reduce((a, b) => a > b ? a : b); } diff --git a/lib/src/extensions/map.dart b/lib/src/extensions/map.dart index 21982df..ffa928c 100644 --- a/lib/src/extensions/map.dart +++ b/lib/src/extensions/map.dart @@ -53,9 +53,7 @@ extension MayrMapExtensions on Map { /// // {'a': 1, 'c': 3} /// ``` Map filterKeys(bool Function(K key) predicate) { - return Map.fromEntries( - entries.where((e) => predicate(e.key)), - ); + return Map.fromEntries(entries.where((e) => predicate(e.key))); } /// Creates a new map with only the entries whose values satisfy [predicate]. @@ -66,9 +64,7 @@ extension MayrMapExtensions on Map { /// // {'b': 2, 'c': 3} /// ``` Map filterValues(bool Function(V value) predicate) { - return Map.fromEntries( - entries.where((e) => predicate(e.value)), - ); + return Map.fromEntries(entries.where((e) => predicate(e.value))); } /// Creates a new map with keys and values swapped. @@ -79,9 +75,7 @@ extension MayrMapExtensions on Map { /// // {1: 'a', 2: 'b'} /// ``` Map invert() { - return Map.fromEntries( - entries.map((e) => MapEntry(e.value, e.key)), - ); + return Map.fromEntries(entries.map((e) => MapEntry(e.value, e.key))); } /// Merges this map with [otherMap], with entries from [otherMap] taking precedence. @@ -163,7 +157,10 @@ extension MayrMapExtensions on Map { /// ``` String toQueryString() { return entries - .map((e) => '${Uri.encodeComponent(e.key.toString())}=${Uri.encodeComponent(e.value.toString())}') + .map( + (e) => + '${Uri.encodeComponent(e.key.toString())}=${Uri.encodeComponent(e.value.toString())}', + ) .join('&'); } } diff --git a/test/extensions/humanize_test.dart b/test/extensions/humanize_test.dart index 6714c54..fe3b8fa 100644 --- a/test/extensions/humanize_test.dart +++ b/test/extensions/humanize_test.dart @@ -8,8 +8,10 @@ void main() { expect(Duration(days: 1).humanize(), '1 day'); expect(Duration(seconds: 45).humanize(), '45 seconds'); expect(Duration(hours: 1, minutes: 30).humanize(), '1 hour, 30 minutes'); - expect(Duration(days: 2, hours: 3, minutes: 15).humanize(), - '2 days, 3 hours, and 15 minutes'); + expect( + Duration(days: 2, hours: 3, minutes: 15).humanize(), + '2 days, 3 hours, and 15 minutes', + ); }); test('humanize handles zero duration', () { @@ -32,17 +34,20 @@ void main() { test('humanize formats past times correctly', () { final now = DateTime.now(); - + // Use a time far enough in the past to avoid timing issues final fiveMinutesAgo = now.subtract(Duration(minutes: 5, seconds: 30)); final result = fiveMinutesAgo.humanize(); expect(result == '5 minutes ago' || result == '6 minutes ago', true); - + // Add extra seconds to avoid boundary timing issues final threeHoursAgo = now.subtract(Duration(hours: 3, seconds: 30)); final hoursResult = threeHoursAgo.humanize(); - expect(hoursResult == '3 hours ago' || hoursResult == '4 hours ago', true); - + expect( + hoursResult == '3 hours ago' || hoursResult == '4 hours ago', + true, + ); + expect(now.subtract(Duration(days: 1)).humanize(), 'yesterday'); expect(now.subtract(Duration(days: 3)).humanize(), '3 days ago'); expect(now.subtract(Duration(days: 10)).humanize(), 'last week'); @@ -50,41 +55,54 @@ void main() { test('humanize formats future times correctly', () { final now = DateTime.now(); - + // Use a time far enough in the future to avoid timing issues final fiveMinutesLater = now.add(Duration(minutes: 5, seconds: 30)); final result = fiveMinutesLater.humanize(); expect(result == 'in 5 minutes' || result == 'in 6 minutes', true); - + // Add extra seconds to avoid boundary timing issues final threeHoursLater = now.add(Duration(hours: 3, seconds: 30)); final hoursResult = threeHoursLater.humanize(); expect(hoursResult == 'in 3 hours' || hoursResult == 'in 4 hours', true); - - expect(now.add(Duration(days: 1)).humanize(), 'tomorrow'); - expect(now.add(Duration(days: 3)).humanize(), 'in 3 days'); - expect(now.add(Duration(days: 10)).humanize(), 'next week'); + + expect(now.add(Duration(days: 1, seconds: 1)).humanize(), 'tomorrow'); + expect(now.add(Duration(days: 3, seconds: 1)).humanize(), 'in 3 days'); + expect(now.add(Duration(days: 10, seconds: 1)).humanize(), 'next week'); }); test('humanize handles singular values', () { final now = DateTime.now(); - + // Add buffer seconds to avoid timing boundary issues final oneMinuteAgo = now.subtract(Duration(minutes: 1, seconds: 30)); final minuteAgoResult = oneMinuteAgo.humanize(); - expect(minuteAgoResult == '1 minute ago' || minuteAgoResult == '2 minutes ago', true); - + expect( + minuteAgoResult == '1 minute ago' || minuteAgoResult == '2 minutes ago', + true, + ); + final oneHourAgo = now.subtract(Duration(hours: 1, seconds: 30)); final hourAgoResult = oneHourAgo.humanize(); - expect(hourAgoResult == '1 hour ago' || hourAgoResult == '2 hours ago', true); - + expect( + hourAgoResult == '1 hour ago' || hourAgoResult == '2 hours ago', + true, + ); + final oneMinuteLater = now.add(Duration(minutes: 1, seconds: 30)); final minuteLaterResult = oneMinuteLater.humanize(); - expect(minuteLaterResult == 'in 1 minute' || minuteLaterResult == 'in 2 minutes', true); - + expect( + minuteLaterResult == 'in 1 minute' || + minuteLaterResult == 'in 2 minutes', + true, + ); + final oneHourLater = now.add(Duration(hours: 1, seconds: 30)); final hourLaterResult = oneHourLater.humanize(); - expect(hourLaterResult == 'in 1 hour' || hourLaterResult == 'in 2 hours', true); + expect( + hourLaterResult == 'in 1 hour' || hourLaterResult == 'in 2 hours', + true, + ); }); }); @@ -93,7 +111,8 @@ void main() { expect(999.humanizeNumber(), '999'); expect(1234.humanizeNumber(), '1.2k'); expect(1500000.humanizeNumber(), '1.5M'); - expect(1000000000.humanizeNumber(), '1.0B'); + expect(1000000000.humanizeNumber(), '1B'); + expect(1100000000.humanizeNumber(), '1.1B'); }); test('humanizeNumber handles decimals', () { @@ -132,10 +151,12 @@ void main() { test('humanizeFileSize formats file sizes correctly', () { expect(0.humanizeFileSize(), '0 B'); expect(500.humanizeFileSize(), '500 B'); - expect(1024.humanizeFileSize(), '1.0 KB'); - expect(1048576.humanizeFileSize(), '1.0 MB'); + expect(1024.humanizeFileSize(), '1 KB'); + expect(1025.humanizeFileSize(), '1.0 KB'); + expect(1126.humanizeFileSize(), '1.1 KB'); + expect(1048576.humanizeFileSize(), '1 MB'); expect(520300.humanizeFileSize(), '508.1 KB'); - expect(1073741824.humanizeFileSize(), '1.0 GB'); + expect(1073741824.humanizeFileSize(), '1 GB'); }); test('humanizeFileSize handles decimals', () { diff --git a/test/extensions/list_test.dart b/test/extensions/list_test.dart index 5440c84..0668a4a 100644 --- a/test/extensions/list_test.dart +++ b/test/extensions/list_test.dart @@ -16,7 +16,10 @@ void main() { test('singleWhereOrNull returns matching element or null', () { expect([1, 2, 3].singleWhereOrNull((e) => e == 2), 2); expect([1, 2, 3].singleWhereOrNull((e) => e > 5), null); - expect([1, 2, 2, 3].singleWhereOrNull((e) => e == 2), null); // Multiple matches + expect( + [1, 2, 2, 3].singleWhereOrNull((e) => e == 2), + null, + ); // Multiple matches }); test('containsWhere checks if predicate is satisfied', () { @@ -117,14 +120,38 @@ void main() { }); test('chunked splits into chunks', () { - expect([1, 2, 3, 4, 5].chunked(2), [[1, 2], [3, 4], [5]]); - expect([1, 2, 3].chunked(1), [[1], [2], [3]]); - expect([1, 2, 3].chunked(5), [[1, 2, 3]]); + expect([1, 2, 3, 4, 5].chunked(2), [ + [1, 2], + [3, 4], + [5], + ]); + expect([1, 2, 3].chunked(1), [ + [1], + [2], + [3], + ]); + expect([1, 2, 3].chunked(5), [ + [1, 2, 3], + ]); }); test('flatten flattens nested lists', () { - expect([[1, 2], [3, 4], [5]].flatten(), [1, 2, 3, 4, 5]); - expect([[], [1], [2, 3]].flatten(), [1, 2, 3]); + expect( + [ + [1, 2], + [3, 4], + [5], + ].flatten(), + [1, 2, 3, 4, 5], + ); + expect( + [ + [], + [1], + [2, 3], + ].flatten(), + [1, 2, 3], + ); }); test('flip reverses the list', () { @@ -147,7 +174,12 @@ void main() { }); test('updateWhere updates matching elements', () { - expect([1, 2, 3, 4].updateWhere((e) => e > 2, (e) => e * 2), [1, 2, 6, 8]); + expect([1, 2, 3, 4].updateWhere((e) => e > 2, (e) => e * 2), [ + 1, + 2, + 6, + 8, + ]); }); test('addIf adds value conditionally', () { @@ -202,41 +234,15 @@ void main() { test('averageBy calculates average', () { expect([1, 2, 3, 4, 5].averageBy((e) => e), 3.0); - expect([].averageBy((e) => e), 0); + expect([].averageBy((e) => e), 0); }); test('min returns minimum', () { expect([3, 1, 4, 1, 5].min(), 1); - expect([].min(), null); }); test('max returns maximum', () { expect([3, 1, 4, 1, 5].max(), 5); - expect([].max(), null); - }); - }); - - group('Comparable Iterable Extensions', () { - test('minBy returns element with minimum key', () { - final list = [ - {'name': 'Alice', 'age': 30}, - {'name': 'Bob', 'age': 25}, - {'name': 'Charlie', 'age': 35}, - ]; - final result = list.minBy((e) => e['age'] as int); - expect(result?['name'], 'Bob'); - expect([].minBy((e) => e), null); - }); - - test('maxBy returns element with maximum key', () { - final list = [ - {'name': 'Alice', 'age': 30}, - {'name': 'Bob', 'age': 25}, - {'name': 'Charlie', 'age': 35}, - ]; - final result = list.maxBy((e) => e['age'] as int); - expect(result?['name'], 'Charlie'); - expect([].maxBy((e) => e), null); }); }); } diff --git a/test/extensions/map_test.dart b/test/extensions/map_test.dart index b4a7dcb..acf4df9 100644 --- a/test/extensions/map_test.dart +++ b/test/extensions/map_test.dart @@ -49,10 +49,10 @@ void main() { }); test('combine merges with custom combiner', () { - final result = {'a': 1, 'b': 2}.combine( - {'b': 3, 'c': 4}, - (k, v1, v2) => v1 + v2, - ); + final result = { + 'a': 1, + 'b': 2, + }.combine({'b': 3, 'c': 4}, (k, v1, v2) => v1 + v2); expect(result, {'a': 1, 'b': 5, 'c': 4}); }); @@ -62,7 +62,8 @@ void main() { }); test('valuesWhere returns values matching predicate', () { - final result = {'a': 1, 'b': 2, 'c': 3}.valuesWhere((k) => k != 'b').toList(); + final result = + {'a': 1, 'b': 2, 'c': 3}.valuesWhere((k) => k != 'b').toList(); expect(result, [1, 3]); }); @@ -72,8 +73,13 @@ void main() { }); test('toQueryString handles special characters', () { - final result = {'key': 'value with spaces', 'special': 'a&b=c'}.toQueryString(); - expect(result.contains('value+with+spaces') || result.contains('value%20with%20spaces'), true); + final result = + {'key': 'value with spaces', 'special': 'a&b=c'}.toQueryString(); + expect( + result.contains('value+with+spaces') || + result.contains('value%20with%20spaces'), + true, + ); }); }); } diff --git a/test/extensions/set_test.dart b/test/extensions/set_test.dart index 69e5635..da426c0 100644 --- a/test/extensions/set_test.dart +++ b/test/extensions/set_test.dart @@ -31,7 +31,10 @@ void main() { }); test('unionAll combines multiple sets', () { - final result = {1, 2}.unionAll([{2, 3}, {3, 4}]); + final result = {1, 2}.unionAll([ + {2, 3}, + {3, 4}, + ]); expect(result, {1, 2, 3, 4}); });