diff --git a/packages/collections/.gitignore b/packages/collections/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/collections/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/collections/CHANGELOG.md b/packages/collections/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/packages/collections/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/collections/LICENSE.md b/packages/collections/LICENSE.md new file mode 100644 index 0000000..0fd0d03 --- /dev/null +++ b/packages/collections/LICENSE.md @@ -0,0 +1,10 @@ +The MIT License (MIT) + +The Laravel Framework is Copyright (c) Taylor Otwell +The Fabric Framework is Copyright (c) Vieo, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/collections/README.md b/packages/collections/README.md new file mode 100644 index 0000000..d0d775f --- /dev/null +++ b/packages/collections/README.md @@ -0,0 +1,91 @@ +# Platform Collections + +A Dart implementation of Laravel-inspired collections, providing a fluent, convenient wrapper for working with arrays of data. + +## Features + +- Chainable methods for manipulating collections of data +- Type-safe operations +- Null-safe implementation +- Inspired by Laravel's collection methods + +## Getting started + +Add this package to your `pubspec.yaml`: + +```yaml +dependencies: + platform_collections: ^1.0.0 +``` + +Then run `dart pub get` or `flutter pub get` to install the package. + +## Usage + +Here's a simple example of how to use the `Collection` class: + +```dart +import 'package:platform_collections/platform_collections.dart'; + +void main() { + final numbers = Collection([1, 2, 3, 4, 5]); + + // Using various collection methods + final result = numbers + .whereCustom((n) => n % 2 == 0) + .mapCustom((n) => n * 2) + .toList(); + + print(result); // [4, 8] + + // Chaining methods + final sum = numbers + .whereCustom((n) => n > 2) + .fold(0, (prev, curr) => prev + curr); + + print(sum); // 12 +} +``` + +## Available Methods + +- `all()`: Returns all items in the collection +- `avg()`: Calculates the average of the collection +- `chunk()`: Chunks the collection into smaller collections +- `collapse()`: Collapses a collection of arrays into a single collection +- `concat()`: Concatenates the given array or collection +- `contains()`: Determines if the collection contains a given item +- `count()`: Returns the total number of items in the collection +- `each()`: Iterates over the items in the collection +- `everyNth()`: Creates a new collection consisting of every n-th element +- `except()`: Returns all items except for those with the specified keys +- `filter()` / `whereCustom()`: Filters the collection using a callback +- `first()` / `firstWhere()`: Returns the first element that passes the given truth test +- `flatten()`: Flattens a multi-dimensional collection +- `flip()`: Flips the items in the collection +- `fold()`: Reduces the collection to a single value +- `groupBy()`: Groups the collection's items by a given key +- `join()`: Joins the items in a collection +- `last()` / `lastOrNull()`: Returns the last element in the collection +- `map()` / `mapCustom()`: Runs a map over each of the items +- `mapSpread()`: Runs a map over each nested chunk of items +- `max()`: Returns the maximum value in the collection +- `merge()`: Merges the given array into the collection +- `min()`: Returns the minimum value in the collection +- `only()`: Returns only the items from the collection with the specified keys +- `pluck()`: Retrieves all of the collection values for a given key +- `random()`: Returns a random item from the collection +- `reverse()`: Reverses the order of the collection's items +- `search()`: Searches the collection for a given value +- `shuffle()`: Shuffles the items in the collection +- `slice()`: Returns a slice of the collection +- `sort()` / `sortCustom()`: Sorts the collection +- `take()`: Takes the first or last {n} items + +## Additional Information + +For more detailed examples, please refer to the `example/collections_example.dart` file in the package. + +If you encounter any issues or have feature requests, please file them on the [issue tracker](https://github.com/yourusername/platform_collections/issues). + +Contributions are welcome! Please read our [contributing guidelines](https://github.com/yourusername/platform_collections/blob/main/CONTRIBUTING.md) before submitting a pull request. diff --git a/packages/collections/analysis_options.yaml b/packages/collections/analysis_options.yaml new file mode 100644 index 0000000..2349c51 --- /dev/null +++ b/packages/collections/analysis_options.yaml @@ -0,0 +1,21 @@ +include: package:lints/recommended.yaml + +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + language: + strict-casts: true + strict-raw-types: true + +linter: + rules: + - always_declare_return_types + - cancel_subscriptions + - close_sinks + - comment_references + - one_member_abstracts + - only_throw_errors + - package_api_docs + - prefer_final_in_for_each + - prefer_single_quotes diff --git a/packages/collections/example/collections_example.dart b/packages/collections/example/collections_example.dart new file mode 100644 index 0000000..2eb34e0 --- /dev/null +++ b/packages/collections/example/collections_example.dart @@ -0,0 +1,65 @@ +import 'package:platform_collections/platform_collections.dart'; + +void main() { + // Create a new collection + final numbers = Collection([1, 2, 3, 4, 5]); + + print('Original collection: ${numbers.all()}'); + + // Demonstrate some collection methods + print('Average: ${numbers.avg()}'); + print('Chunks of 2: ${numbers.chunk(2).map((chunk) => chunk.all())}'); + print('Every 2nd item: ${numbers.everyNth(2).all()}'); + print('Except indices [1, 3]: ${numbers.except([1, 3]).all()}'); + print('First even number: ${numbers.firstWhere((n) => n % 2 == 0)}'); + print('Reversed: ${numbers.reverse().all()}'); + + // Demonstrate map and filter operations + final doubled = numbers.mapCustom((n) => n * 2); + print('Doubled: ${doubled.all()}'); + + final evenNumbers = numbers.whereCustom((n) => n % 2 == 0); + print('Even numbers: ${evenNumbers.all()}'); + + // Demonstrate reduce operation + final sum = numbers.fold(0, (prev, curr) => prev + curr); + print('Sum: $sum'); + + // Demonstrate sorting + final sortedDesc = numbers.sortCustom((a, b) => b.compareTo(a)); + print('Sorted descending: ${sortedDesc.all()}'); + + // Demonstrate search + final searchResult = numbers.search(3); + print('Index of 3: $searchResult'); + + // Demonstrate JSON conversion + print('JSON representation: ${numbers.toJson()}'); + + // Demonstrate operations with non-numeric collections + final fruits = Collection(['apple', 'banana', 'cherry', 'date']); + print('\nFruits: ${fruits.all()}'); + print( + 'Fruits starting with "b": ${fruits.whereCustom((f) => f.startsWith('b')).all()}'); + print( + 'Fruit names in uppercase: ${fruits.mapCustom((f) => f.toUpperCase()).all()}'); + + // Demonstrate nested collections + final nested = Collection([ + [1, 2], + [3, 4], + [5, 6], + ]); + print('\nNested collection: ${nested.all()}'); + print('Flattened: ${nested.flatten().all()}'); + + // Demonstrate grouping + final people = Collection([ + {'name': 'Alice', 'age': 25}, + {'name': 'Bob', 'age': 30}, + {'name': 'Charlie', 'age': 25}, + {'name': 'David', 'age': 30}, + ]); + final groupedByAge = people.groupBy((person) => person['age']); + print('\nPeople grouped by age: $groupedByAge'); +} diff --git a/packages/collections/lib/collections.dart b/packages/collections/lib/collections.dart new file mode 100644 index 0000000..597ab0b --- /dev/null +++ b/packages/collections/lib/collections.dart @@ -0,0 +1,3 @@ +library collections; + +export 'src/collection.dart'; diff --git a/packages/collections/lib/platform_collections.dart b/packages/collections/lib/platform_collections.dart new file mode 100644 index 0000000..47101e8 --- /dev/null +++ b/packages/collections/lib/platform_collections.dart @@ -0,0 +1,3 @@ +library platform_collections; + +export 'src/collection.dart'; diff --git a/packages/collections/lib/src/collection.dart b/packages/collections/lib/src/collection.dart new file mode 100644 index 0000000..9af9f09 --- /dev/null +++ b/packages/collections/lib/src/collection.dart @@ -0,0 +1,262 @@ +import 'dart:collection'; +import 'dart:math'; + +/// A collection class inspired by Laravel's Collection, implemented in Dart. +class Collection with ListMixin { + final List _items; + + /// Creates a new [Collection] instance. + Collection([Iterable? items]) : _items = List.from(items ?? []); + + /// Creates a new [Collection] instance from a [Map]. + factory Collection.fromMap(Map map) { + return Collection(map.values); + } + + @override + int get length => _items.length; + + @override + set length(int newLength) { + _items.length = newLength; + } + + @override + T operator [](int index) => _items[index]; + + @override + void operator []=(int index, T value) { + _items[index] = value; + } + + /// Returns all items in the collection. + List all() => _items.toList(); + + /// Returns the average value of the collection. + double? avg([num Function(T element)? callback]) { + if (isEmpty) return null; + num sum = 0; + for (final item in _items) { + sum += callback != null ? callback(item) : (item as num); + } + return sum / length; + } + + /// Chunks the collection into smaller collections of a given size. + Collection> chunk(int size) { + return Collection( + List.generate( + (length / size).ceil(), + (index) => Collection(_items.skip(index * size).take(size)), + ), + ); + } + + /// Collapses a collection of arrays into a single, flat collection. + Collection collapse() { + return Collection(_items.expand((e) => e is Iterable ? e : [e])); + } + + /// Determines whether the collection contains a given item. + @override + bool contains(Object? item) => _items.contains(item); + + /// Returns the total number of items in the collection. + int count() => length; + + /// Executes a callback over each item. + void each(void Function(T item) callback) { + for (final item in _items) { + callback(item); + } + } + + /// Creates a new collection consisting of every n-th element. + Collection everyNth(int step) { + return Collection(_items.where((item) => _items.indexOf(item) % step == 0)); + } + + /// Returns all items except for those with the specified keys. + Collection except(List keys) { + return Collection( + _items.where((item) => !keys.contains(_items.indexOf(item)))); + } + + /// Filters the collection using the given callback. + Collection whereCustom(bool Function(T element) test) { + return Collection(_items.where(test)); + } + + /// Returns the first element in the collection that passes the given truth test. + @override + T firstWhere(bool Function(T element) test, {T Function()? orElse}) { + return _items.firstWhere(test, orElse: orElse); + } + + /// Flattens a multi-dimensional collection into a single dimension. + Collection flatten({int depth = 1}) { + List flattenHelper(dynamic item, int currentDepth) { + if (currentDepth == 0 || item is! Iterable) return [item]; + return item.expand((e) => flattenHelper(e, currentDepth - 1)).toList(); + } + + return Collection( + flattenHelper(_items, depth).expand((e) => e is Iterable ? e : [e])); + } + + /// Flips the items in the collection. + Collection flip() { + return Collection(_items.reversed); + } + + /// Removes an item from the collection by its key. + T? pull(int index) { + if (index < 0 || index >= length) return null; + return removeAt(index); + } + + /// Concatenates the given array or collection with the original collection. + Collection concat(Iterable items) { + return Collection([..._items, ...items]); + } + + /// Reduces the collection to a single value. + @override + U fold(U initialValue, U Function(U previousValue, T element) combine) { + return _items.fold(initialValue, combine); + } + + /// Groups the collection's items by a given key. + Map> groupBy(K Function(T element) keyFunction) { + return _items.fold>>({}, (map, element) { + final key = keyFunction(element); + map.putIfAbsent(key, () => []).add(element); + return map; + }); + } + + /// Joins the items in a collection with a string. + @override + String join([String separator = '']) => _items.join(separator); + + /// Returns a new collection with the keys of the collection items. + Collection keys() => Collection(List.generate(length, (index) => index)); + + /// Returns the last element in the collection. + T? lastOrNull() => isNotEmpty ? _items.last : null; + + /// Runs a map over each of the items. + Collection mapCustom(R Function(T e) toElement) { + return Collection(_items.map(toElement)); + } + + /// Run a map over each nested chunk of items. + Collection mapSpread(R Function(dynamic e) toElement) { + return Collection(_items + .expand((e) => e is Iterable ? e.map(toElement) : [toElement(e)])); + } + + /// Returns the maximum value of a given key. + T? max([Comparable Function(T element)? callback]) { + if (isEmpty) return null; + return _items.reduce((a, b) { + final compareA = + callback != null ? callback(a) : a as Comparable; + final compareB = + callback != null ? callback(b) : b as Comparable; + return compareA.compareTo(compareB) > 0 ? a : b; + }); + } + + /// Returns the minimum value of a given key. + T? min([Comparable Function(T element)? callback]) { + if (isEmpty) return null; + return _items.reduce((a, b) { + final compareA = + callback != null ? callback(a) : a as Comparable; + final compareB = + callback != null ? callback(b) : b as Comparable; + return compareA.compareTo(compareB) < 0 ? a : b; + }); + } + + /// Returns only the items from the collection with the specified keys. + Collection only(List keys) { + return Collection( + _items.where((item) => keys.contains(_items.indexOf(item)))); + } + + /// Retrieves all of the collection values for a given key. + Collection pluck(R Function(T element) callback) { + return Collection(_items.map(callback)); + } + + /// Removes and returns the last item from the collection. + T? pop() => isNotEmpty ? removeLast() : null; + + /// Adds an item to the beginning of the collection. + void prepend(T value) => insert(0, value); + + /// Adds an item to the end of the collection. + void push(T value) => add(value); + + /// Returns a random item from the collection. + T? random() => isEmpty ? null : this[_getRandomIndex()]; + + /// Reverses the order of the collection's items. + Collection reverse() => Collection(_items.reversed); + + /// Searches the collection for a given value and returns the corresponding key if successful. + int? search(T item, {bool Function(T, T)? compare}) { + compare ??= (a, b) => a == b; + final index = _items.indexWhere((element) => compare!(element, item)); + return index != -1 ? index : null; + } + + /// Shuffles the items in the collection. + @override + void shuffle([Random? random]) { + _items.shuffle(random); + } + + /// Returns a slice of the collection starting at the given index. + Collection slice(int offset, [int? length]) { + return Collection( + _items.skip(offset).take(length ?? _items.length - offset)); + } + + /// Sorts the collection. + Collection sortCustom([int Function(T a, T b)? compare]) { + final sorted = [..._items]; + sorted.sort(compare); + return Collection(sorted); + } + + /// Takes the first or last {n} items. + @override + Collection take(int count) { + if (count < 0) { + return Collection(_items.skip(_items.length + count)); + } + return Collection(_items.take(count)); + } + + /// Returns a JSON representation of the collection. + String toJson() => + '[${_items.map((e) => e is Map ? _mapToJson(e as Map) : e.toString()).join(',')}]'; + + /// Merges the given array or collection with the original collection. + Collection merge(Iterable items) { + return Collection([..._items, ...items]); + } + + // Helper methods + int _getRandomIndex() => + (DateTime.now().millisecondsSinceEpoch % length).abs(); + + String _mapToJson(Map map) { + final pairs = map.entries.map((e) => + '"${e.key}":${e.value is Map ? _mapToJson(e.value as Map) : '"${e.value}"'}'); + return '{${pairs.join(',')}}'; + } +} diff --git a/packages/collections/pubspec.yaml b/packages/collections/pubspec.yaml new file mode 100644 index 0000000..83d13da --- /dev/null +++ b/packages/collections/pubspec.yaml @@ -0,0 +1,13 @@ +name: platform_collections +description: A Dart implementation of Laravel-inspired collections. +version: 1.0.0 +homepage: https://github.com/yourusername/collections + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: {} + +dev_dependencies: + test: ^1.16.0 + lints: ^2.0.0 diff --git a/packages/collections/test/collection_test.dart b/packages/collections/test/collection_test.dart new file mode 100644 index 0000000..a836fda --- /dev/null +++ b/packages/collections/test/collection_test.dart @@ -0,0 +1,84 @@ +import 'package:platform_collections/platform_collections.dart'; +import 'package:test/test.dart'; + +void main() { + group('Collection', () { + test('creates a collection from a list', () { + final collection = Collection([1, 2, 3]); + expect(collection.all(), equals([1, 2, 3])); + }); + + test('avg calculates the average', () { + final collection = Collection([1, 2, 3, 4, 5]); + expect(collection.avg(), equals(3)); + }); + + test('chunk splits the collection into smaller collections', () { + final collection = Collection([1, 2, 3, 4, 5]); + final chunked = collection.chunk(2); + expect( + chunked.map((c) => c.all()), + equals([ + [1, 2], + [3, 4], + [5] + ])); + }); + + test('whereCustom filters the collection', () { + final collection = Collection([1, 2, 3, 4, 5]); + final filtered = collection.whereCustom((n) => n % 2 == 0); + expect(filtered.all(), equals([2, 4])); + }); + + test('mapCustom transforms the collection', () { + final collection = Collection([1, 2, 3]); + final mapped = collection.mapCustom((n) => n * 2); + expect(mapped.all(), equals([2, 4, 6])); + }); + + test('fold reduces the collection', () { + final collection = Collection([1, 2, 3, 4, 5]); + final sum = collection.fold(0, (prev, curr) => prev + (curr as int)); + expect(sum, equals(15)); + }); + + test('sortCustom sorts the collection', () { + final collection = Collection([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]); + final sorted = collection.sortCustom((a, b) => a.compareTo(b)); + expect(sorted.all(), equals([1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9])); + }); + + test('flatten flattens nested collections', () { + final collection = Collection([ + [1, 2], + [3, 4], + [5, 6] + ]); + final flattened = collection.flatten(); + expect(flattened.all(), equals([1, 2, 3, 4, 5, 6])); + }); + + test('groupBy groups collection items', () { + final collection = Collection([ + {'name': 'Alice', 'age': 25}, + {'name': 'Bob', 'age': 30}, + {'name': 'Charlie', 'age': 25}, + {'name': 'David', 'age': 30}, + ]); + final grouped = collection.groupBy((item) => item['age']); + expect( + grouped, + equals({ + 25: [ + {'name': 'Alice', 'age': 25}, + {'name': 'Charlie', 'age': 25} + ], + 30: [ + {'name': 'Bob', 'age': 30}, + {'name': 'David', 'age': 30} + ] + })); + }); + }); +}