commit 92e792a02268417ceaedec3970095d8ff4feb170 Author: Hans Kokx Date: Tue Sep 10 18:46:37 2024 +0200 Initial release Signed-off-by: Hans Kokx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..fe420e9 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "4cf269e36de2573851eaef3c763994f8f9be494d" + channel: "stable" + +project_type: package diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..32f490b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7e8b25b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +Contributing +============ + +[Improvements, bug reports, feature requests welcome][gh-issues]. + +- Please include `dart --version` and the package version when reporting bugs. +- Code should be formatted with `dartfmt`. +- Public methods should have doc comments and test coverage. +- Itemize user-facing changes in the `HEAD` section of the `CHANGELOG` file. +- Use [well-formatted commit messages][git-log-fmt]. + +[gh-issues]: https://github.com/hanskokx/arcane_helper_utils/issues +[git-log-fmt]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aea1567 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Hans Kokx + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9eab204 --- /dev/null +++ b/README.md @@ -0,0 +1,317 @@ +# Overview + +Arcane Helper Utils is a Dart package designed to enhance Dart development by +providing utility functions and extensions that simplify common tasks. + +## Features + +- **Ticker Utility**: A utility class that facilitates time-based actions, + perfect for animations or any timing-related operations. +- **JSON Converter**: Simplifies the process of converting JSON data into + Dart objects. +- **DateTime Extensions**: Adds additional functionality to the `DateTime` + class, making it easier to format dates and calculate differences. +- **String Extensions**: Enhances the `String` class by adding new methods for + common transformations and checks, including JWT parsing. + +## Getting Started + +To use this package in your Dart project, add it to your project's +`pubspec.yaml` file: + +```yaml +dependencies: + arcane_helper_utils: any +``` + +Then import it in your Dart files where needed: + +```dart +import 'package:arcane_helper_utils/arcane_helper_utils.dart'; +``` + +## Usage Examples + +Here are some examples of how to use the utilities and extensions provided by +this package: + +### Ticker + +The `Ticker` can be used as a countdown or interval timer. + +```dart +final Stream ticker = const Ticker().tick( + timeout: Duration(seconds: 30), + interval: Duration(seconds: 5) +); + +await for (final int ticksRemaining in ticker) { + if (ticksRemaining == 0) print("Time's up!"); + print('Tick! $ticksRemaining'); +} +``` + +### JSON Conversion + +These helper methods are used in conjunction with the Freezed package to +annotate fields that need to be converted from one data type to another. +The available conversions are: + +- `String?` to `int?` +- `String?` to `double?` + +Provided the following JSON output, the `valueIsMaybeNull` field will be +converted from an empty `String` to `null`, the `valueIsDouble` field will +be converted from a `String?` to a `double?`, and the `valueIsInt` field will be +converted from a `String?` to an `int?`: + +```json +{ + "valueIsMaybeNull": "", + "valueIsDouble": "123.456", + "valueIsInt": "123" +} +``` + +```dart +@freezed +class MyFreezedClass with _$MyFreezedClass { + const factory MyFreezedClass({ + @DecimalConverter() double? valueIsMaybeNull, + @DecimalConverter() double? valueIsDouble, + @IntegerConverter() int? valueIsInt, + }) = _MyFreezedClass; + + factory MyFreezedClass.fromJson(Map json) => + _$MyFreezedClassFromJson(json); + + const MyFreezedClass._(); +} +``` + +### DateTime Extensions + +These extensions add helpful methods to the `DateTime` class, making it easier +to handle common date and time operations such as formatting, comparisons, and +calculations. + +These are broken down into the following categories: + +- Start and end of period calculations +- Comparison operations +- Period information operations + +#### Start and End of Period Calculations + +The following operations are now available on a `DateTime` object: + +- `startOfHour`: Returns a new `DateTime` object where the time stamp is set to + the beginning of the given hour. + + ```dart + final DateTime dateTime = DateTime(2019, 1, 1, 13, 45); + print(dateTime); // 2019-01-01T13:45:00.0 + print(dateTime.startOfHour); // 2019-01-01T13:00:00.0 + ``` + +- `endOfHour`: Returns a new `DateTime` object where the time stamp is set to + the end of the given hour. + + ```dart + final DateTime dateTime = DateTime(2019, 1, 1, 13, 45); + print(dateTime); // 2019-01-01T13:45:00.0 + print(dateTime.endOfHour); // 2019-01-01T13:59:59.999 + ``` + +- `startOfDay`: Returns a new `DateTime` object where the time stamp is set to + the beginning of the day. + + ```dart + final DateTime dateTime = DateTime(2019, 1, 1, 13, 45); + print(dateTime); // 2019-01-01T13:45:00.0 + print(dateTime.startOfDay); // 2019-01-01T00:00:00.0 + ``` + +- `endOfDay`: Returns a new `DateTime` object where the time stamp is set to the + end of the day. + + ```dart + final DateTime dateTime = DateTime(2019, 1, 1, 13, 45); + print(dateTime); // 2019-01-01T13:45:00.0 + print(dateTime.endOfDay); // 2019-01-01T23:59:59.9 + ``` + +- `startOfWeek`: Returns a new `DateTime` object where the time stamp is set to + the beginning of the week (Monday). + + ```dart + final DateTime dateTime = DateTime(2023, 9, 10); + print(dateTime); // 2023-09-10 + print(dateTime.startOfWeek); // 2023-09-04 + ``` + +- `endOfWeek`: Returns a new `DateTime` object where the time stamp is set to + the end of the week (Sunday). + + ```dart + final DateTime dateTime = DateTime(2023, 9, 10); + print(dateTime); // 2023-09-10 + print(dateTime.endOfWeek); // 2023-09-17T23:59:59.999999 + ``` + +- `startOfMonth`: Returns a new `DateTime` object where the time stamp is set to + the beginning of the month. + + ```dart + final DateTime dateTime = DateTime(2023, 9, 10); + print(dateTime); // 2023-09-10 + print(dateTime.startOfMonth); // 2023-09-01T00:00:00.0 + ``` + +- `endOfMonth`: Returns a new `DateTime` object where the time stamp is set to + the end of the month. + + ```dart + final DateTime dateTime = DateTime(2023, 9, 10); + print(dateTime); // 2023-09-10 + print(dateTime.endOfMonth); // 2023-09-30T23:59:59.999999 + ``` + +- `startOfYear`: Returns a new `DateTime` object where the time stamp is set to + the beginning of the year. + + ```dart + final DateTime dateTime = DateTime(2023, 9, 10); + print(dateTime); // 2023-09-10 + print(dateTime.startOfYear); // 2023-01-01T00:00:00.0 + ``` + +- `endOfYear`: Returns a new `DateTime` object where the time stamp is set to + the end of the year. + + ```dart + final DateTime dateTime = DateTime(2023, 9, 10); + print(dateTime); // 2023-09-10 + print(dateTime.endOfYear); // 2023-12-31T23:59:59.999999 + ``` + +#### Comparison Operations + +- `isToday`: Returns `true` if the provided `DateTime` is today, otherwise + returns `false`. + + ```dart + final DateTime today = DateTime(2024, 9, 10); + final bool notToday = DateTime(2001, 12, 31).isToday; // false + ``` + +- `isSameDayAs`: Compares two `DateTime` objects and returns `true` if they + represent the same day. + + ```dart + final DateTime first = DateTime(2001, 1, 1); + final DateTime second = DateTime(2001, 1, 2); + final DateTime third = DateTime(2001, 1, 1); + + final bool firstAndSecond = first.isSameDayAs(second); // false + final bool firstAndThird = first.isSameDayAs(third); // true + ``` + +#### Period Information Operations + +- `daysInMonth`: Returns an `int` with the number of days in a provided month. + + ```dart + final int daysInMonth = DateTime(2024, 9).daysInMonth; // 30 + ``` + +- `firstDayOfWeek`: Returns a new `DateTime` object where the time stamp is set + to the beginning of the first day (Sunday) of the original `DateTime`'s week. + + ```dart + final DateTime today = DateTime(2024, 9, 10); // Tuesday + final DateTime sunday = today.firstDayOfWeek; // Sunday + ``` + +### JWT Parsing + +These extensions enhance the `String` class with JWT-specific functionalities, +making it easier to handle JSON Web Tokens directly as `String` objects. + +Here are some examples of how these methods can be utilized: + +- Extracting the email address (`jwt["sub"]`) + + ```dart + String jwt = "your.jwt.token"; + final String? email = jwt.jwtEmail(); // Returns the email address in the JWT + ``` + +- Extracting the token expiration time (`jwt["exp"]`) + + ```dart + String jwt = "your.jwt.token"; + // Returns a `DateTime?` when the token expires + final DateTime? email = jwt.jwtExpiryTime(); + ``` + +- Extracting the user ID (`jwt["uid"]`) + + ```dart + String jwt = "your.jwt.token"; + final String? uid = jwt.jwtUserId(); // Returns the UID value from the token + ``` + +### String Utilities + +The following utilities have been added to enhance working with `String` +objects: + +- `isNullOrEmpty`: Returns `true` if a `String?` is either null or consists of + only whitespace. + + ```dart + const String? nullString = null; + const String? emptyString = " "; + const String? nonEmptyString = "Hello World!"; + + print(nullString.isNullOrEmpty) // true + print(emptyString.isNullOrEmpty) // true + print(nonEmptyString.isNullOrEmpty) // false + ``` + +- `isNotNullOrEmpty`: Returns `true` if a `String?` is neither null nor consists + of only whitespace. + + ```dart + const String? nullString = null; + const String? emptyString = " "; + const String? nonEmptyString = "Hello World!"; + + print(nullString.isNotNullOrEmpty) // false + print(emptyString.isNotNullOrEmpty) // false + print(nonEmptyString.isNotNullOrEmpty) // true + ``` + +- `splitByLength(int length)`: Splits a `String` into a `List` where + each value is of the maximum length provided. + + ```dart + const String text = "DartLang"; + final List result = text.splitByLength(3); // ["Dar", "tLa", "ng"] + ``` + +- `capitalize`: Capitalizes the first letter of a given `String` + + ```dart + const String text = "hello"; + final String capitalized = text.capitalize; // "Hello" + ``` + +Additionally, the `CommonString` class provides a quick shortcut to common +strings, such as punctuation marks that are otherwise cumbersome to find or type. + +## Contributing + +Contributions are welcome! Feel free to fork the repository and submit pull +requests. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..6eade8d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,117 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + # treat missing required parameters as an error (not a hint) + missing_required_param: error + # treat missing returns as an error (not a hint) + missing_return: error + invalid_annotation_target: ignore + language: + strict-casts: true + +linter: + rules: + always_declare_return_types: true + always_put_required_named_parameters_first: true + annotate_overrides: true + avoid_annotating_with_dynamic: true + avoid_dynamic_calls: true + avoid_escaping_inner_quotes: true + avoid_function_literals_in_foreach_calls: true + avoid_null_checks_in_equality_operators: true + avoid_print: true + avoid_relative_lib_imports: true + avoid_setters_without_getters: true + avoid_shadowing_type_parameters: true + avoid_single_cascade_in_expression_statements: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + camel_case_extensions: true + camel_case_types: true + cancel_subscriptions: true + close_sinks: true + collection_methods_unrelated_type: true + constant_identifier_names: true + control_flow_in_finally: true + depend_on_referenced_packages: true + directives_ordering: true + empty_constructor_bodies: true + empty_statements: true + eol_at_end_of_file: true + exhaustive_cases: true + file_names: true + flutter_style_todos: true + hash_and_equals: true + implementation_imports: true + implicit_call_tearoffs: true + leading_newlines_in_multiline_strings: true + missing_whitespace_between_adjacent_strings: true + no_adjacent_strings_in_list: true + no_duplicate_case_values: true + no_leading_underscores_for_library_prefixes: true + no_leading_underscores_for_local_identifiers: true + no_logic_in_create_state: true + no_runtimeType_toString: true + non_constant_identifier_names: true + null_check_on_nullable_type_parameter: true + null_closures: true + only_throw_errors: true + package_prefixed_library_names: true + prefer_adjacent_string_concatenation: true + prefer_asserts_in_initializer_lists: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_const_constructors_in_immutables: true + prefer_const_constructors: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_contains: true + prefer_double_quotes: true + prefer_final_fields: true + prefer_final_in_for_each: true + prefer_final_locals: true + prefer_for_elements_to_map_fromIterable: true + prefer_function_declarations_over_variables: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_initializing_formals: true + prefer_inlined_adds: true + prefer_interpolation_to_compose_strings: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_is_not_operator: true + prefer_iterable_whereType: true + prefer_null_aware_operators: true + prefer_spread_collections: true + prefer_typing_uninitialized_variables: true + provide_deprecation_message: true + recursive_getters: true + require_trailing_commas: true + sized_box_for_whitespace: true + sized_box_shrink_expand: true + slash_for_doc_comments: true + sort_child_properties_last: true + sort_pub_dependencies: true + type_init_formals: true + type_literal_in_constant_pattern: true + unawaited_futures: true + unnecessary_await_in_return: true + unnecessary_brace_in_string_interps: true + unnecessary_const: true + unnecessary_constructor_name: true + unnecessary_getters_setters: true + unnecessary_late: true + unnecessary_new: true + unnecessary_null_aware_assignments: true + unnecessary_null_in_if_null_operators: true + unnecessary_nullable_for_final_variable_declarations: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_statements: true + use_build_context_synchronously: true + use_colored_box: true + use_key_in_widget_constructors: true + valid_regexps: true diff --git a/lib/arcane_helper_utils.dart b/lib/arcane_helper_utils.dart new file mode 100644 index 0000000..8a9a47e --- /dev/null +++ b/lib/arcane_helper_utils.dart @@ -0,0 +1,6 @@ +library arcane_helper_utils; + +export "package:arcane_helper_utils/src/extensions/date_time.dart"; +export "package:arcane_helper_utils/src/extensions/string.dart"; +export "package:arcane_helper_utils/src/extensions/string_jwt.dart"; +export "package:arcane_helper_utils/src/utils/ticker.dart"; diff --git a/lib/src/extensions/date_time.dart b/lib/src/extensions/date_time.dart new file mode 100644 index 0000000..204f716 --- /dev/null +++ b/lib/src/extensions/date_time.dart @@ -0,0 +1,111 @@ +import "package:week_number/iso.dart"; + +/// An extension on `DateTime` to get the start and end of various time periods. +extension StartAndEndOfPeriod on DateTime { + /// Returns a `DateTime` object representing the start of the hour. + /// + /// The time is set to the beginning of the current hour with minutes, seconds, + /// and milliseconds set to zero. + DateTime get startOfHour => DateTime(year, month, day, hour); + + /// Returns a `DateTime` object representing the end of the hour. + /// + /// The time is set to the last microsecond of the current hour. + DateTime get endOfHour => DateTime(year, month, day, hour) + .add(const Duration(hours: 1)) + .subtract(const Duration(microseconds: 1)); + + /// Returns a `DateTime` object representing the start of the day. + /// + /// The time is set to 00:00 (midnight) of the current day. + DateTime get startOfDay => DateTime(year, month, day); + + /// Returns a `DateTime` object representing the end of the day. + /// + /// The time is set to the last microsecond of the current day, 23:59:59.999999. + DateTime get endOfDay => startOfDay + .add(const Duration(days: 1)) + .subtract(const Duration(microseconds: 1)); + + /// Returns a `DateTime` object representing the end of the current week. + /// + /// The time is set to the end of the last day of the current week (Sunday). + DateTime get endOfWeek => endOfDay.add( + Duration( + days: DateTime.daysPerWeek - weekday, + ), + ); + + /// Returns a `DateTime` object representing the end of the current month. + /// + /// The time is set to the last microsecond of the last day of the current month. + DateTime get endOfMonth => endOfDay.add( + Duration( + days: daysInMonth - day, + ), + ); + + /// Returns a `DateTime` object representing the start of the current week. + /// + /// The time is set to the start of the first day of the current week (Monday). + DateTime get startOfWeek => startOfDay.subtract( + Duration( + days: weekday - DateTime.monday, + ), + ); + + /// Returns a `DateTime` object representing the start of the current month. + /// + /// The time is set to the beginning of the first day of the current month. + DateTime get startOfMonth => DateTime( + year, + month, + 1, + ).startOfDay; + + /// Returns a `DateTime` object representing the start of the current year. + /// + /// The time is set to the beginning of the first day of the current year. + DateTime get startOfYear => DateTime(year).startOfDay; + + /// Returns a `DateTime` object representing the end of the current year. + /// + /// The time is set to the last microsecond of the last day of the current year. + DateTime get endOfYear => startOfYear + .add( + Duration(days: this.isLeapYear ? 365 : 364), + ) + .endOfDay; +} + +/// An extension on `DateTime` to get the number of days in the current month. +extension DaysInMonth on DateTime { + /// Returns the number of days in the current month. + /// + /// It correctly handles leap years and different month lengths. + int get daysInMonth => DateTime(year, month + 1, 0).day; +} + +/// An extension on `DateTime` to check if a date is today or if it is the same day as another date. +extension IsToday on DateTime { + /// Returns `true` if the current date is today. + bool get isToday => DateTime.now().difference(this).inDays == 0; + + /// Returns `true` if the current date is the same day as [other]. + bool isSameDayAs(DateTime other) => + DateTime(other.year, other.month, other.day) + .isAtSameMomentAs(DateTime(year, month, day)); +} + +/// An extension on `DateTime` to get the first day of the current week. +extension FirstDayOfWeek on DateTime { + /// Returns a `DateTime` object representing the first day of the current week (Monday). + /// + /// The time is set to the start of that day. + DateTime get firstDayOfWeek { + final int daysToSubtract = weekday - DateTime.monday; + final DateTime firstDay = + subtract(Duration(days: daysToSubtract)).startOfDay; + return firstDay; + } +} diff --git a/lib/src/extensions/string.dart b/lib/src/extensions/string.dart new file mode 100644 index 0000000..2110339 --- /dev/null +++ b/lib/src/extensions/string.dart @@ -0,0 +1,61 @@ +/// Provides a quick shortcut to common strings, such as punctuation marks that are otherwise +/// cumbersome to find or type. +abstract class CommonString { + /// An em dash (`—`) is commonly used in typography to set off parenthetical + /// phrases or provide emphasis in a sentence. + static const String emDash = "—"; +} + +extension Nullability on String? { + /// Returns `true` if the `String?` is neither `null` nor only contains whitespace. + bool get isNotNullOrEmpty => !isNullOrEmpty; + + /// Returns `true` if the `String?` is either `null` or contains only whitespace. + bool get isNullOrEmpty => this == null || (this ?? "").trim().isEmpty; +} + +/// An extension on `String` to split the string into parts of a specified length. +extension Split on String { + /// Splits the string into a list of substrings, each with a maximum length of [length]. + /// + /// This method divides the string into chunks of size [length]. If the string's + /// length is not a multiple of [length], the last chunk may be smaller than [length]. + /// + /// - [length]: The number of characters in each part. + /// + /// Returns a list of substrings where each has a maximum of [length] characters. + /// + /// Example: + /// ```dart + /// String text = "DartLang"; + /// List result = text.splitByLength(3); // ["Dar", "tLa", "ng"] + /// ``` + List splitByLength(int length) { + final List parts = []; + String string = this; + + while (string.isNotEmpty) { + parts.add(string.substring(0, length)); + string = string.substring(length); + } + return parts; + } +} + +/// An extension on `String` to perform text manipulation tasks. +extension TextManipulation on String { + /// Capitalizes the first letter of the string. + /// + /// This method returns a new string where the first character is converted + /// to uppercase, while the rest of the string remains unchanged. + /// + /// Example: + /// ```dart + /// String text = "hello"; + /// String capitalized = text.capitalize; // "Hello" + /// ``` + String get capitalize { + if (isEmpty) return ""; + return "${this[0].toUpperCase()}${substring(1)}"; + } +} diff --git a/lib/src/extensions/string_jwt.dart b/lib/src/extensions/string_jwt.dart new file mode 100644 index 0000000..05de14d --- /dev/null +++ b/lib/src/extensions/string_jwt.dart @@ -0,0 +1,132 @@ +import "dart:convert"; + +/// An extension on `String` to extract useful information from JSON Web Tokens (JWT). +/// +/// This extension provides methods to decode a JWT string and retrieve common +/// fields like email, expiration time, and user ID. +extension JWTUtility on String { + /// Extracts the email from the JWT payload. + /// + /// This method attempts to parse the JWT and retrieve the `sub` field, which is + /// typically used to store the email of the token owner. + /// + /// Returns the email as a `String` if present, or `null` if the parsing fails or the + /// email is not found. + /// + /// Example: + /// ```dart + /// String token = "your.jwt.token"; + /// String? email = token.jwtEmail(); + /// ``` + String? jwtEmail() { + try { + final payload = _parseJwt(this); + final String email = payload["sub"] as String; + return email; + } catch (_) { + return null; + } + } + + /// Extracts the expiration time from the JWT payload. + /// + /// This method retrieves the `exp` field from the JWT, which represents the expiration + /// time as a Unix timestamp. The timestamp is converted to a `DateTime` object. + /// + /// Returns the `DateTime` representing the expiration time, or `null` if the parsing + /// fails or the expiration time is not found. + /// + /// Example: + /// ```dart + /// String token = "your.jwt.token"; + /// DateTime? expiry = token.jwtExpiryTime(); + /// ``` + DateTime? jwtExpiryTime() { + try { + final payload = _parseJwt(this); + final expiry = payload["exp"] as int; + final date = DateTime.fromMillisecondsSinceEpoch(expiry * 1000); + return date; + } catch (_) { + return null; + } + } + + /// Extracts the user ID from the JWT payload. + /// + /// This method retrieves the `uid` field, which is typically the unique user identifier. + /// This ID can be used in analytics or for tracking purposes. + /// + /// Returns the user ID as a `String`, or `null` if the parsing fails or the ID is not found. + /// + /// Example: + /// ```dart + /// String token = "your.jwt.token"; + /// String? userId = token.jwtUserId(); + /// ``` + String? jwtUserId() { + try { + final payload = _parseJwt(this); + final String id = payload["uid"] as String; + return id; + } catch (_) { + return null; + } + } + + /// Decodes a base64 URL-encoded string. + /// + /// This method replaces the URL-safe characters in the base64 string (`-` and `_`) + /// with standard base64 characters (`+` and `/`), then decodes the string into UTF-8. + /// + /// Throws an `Exception` if the base64 string is not properly padded or is invalid. + /// + /// Example: + /// ```dart + /// String decoded = _decodeBase64("your_base64_string"); + /// ``` + String _decodeBase64(String str) { + String output = str.replaceAll("-", "+").replaceAll("_", "/"); + switch (output.length % 4) { + case 0: + break; + case 2: + output += "=="; + break; + case 3: + output += "="; + break; + default: + throw Exception('Illegal base64url string!"'); + } + + return utf8.decode(base64Url.decode(output)); + } + + /// Parses the JWT and returns the payload as a map. + /// + /// A JWT consists of three parts: header, payload, and signature, separated by dots (`.`). + /// This method decodes the payload (the second part) from base64 and converts it into + /// a `Map`. + /// + /// Throws an `Exception` if the token is not valid or the payload is not a proper JSON map. + /// + /// Example: + /// ```dart + /// Map payload = _parseJwt("your.jwt.token"); + /// ``` + Map _parseJwt(String token) { + final parts = token.split("."); + if (parts.length != 3) { + throw Exception("invalid token"); + } + + final payload = _decodeBase64(parts[1]); + final dynamic payloadMap = json.decode(payload); + if (payloadMap is! Map) { + throw Exception("invalid payload"); + } + + return payloadMap; + } +} diff --git a/lib/src/utils/json_converter.dart b/lib/src/utils/json_converter.dart new file mode 100644 index 0000000..8b68e95 --- /dev/null +++ b/lib/src/utils/json_converter.dart @@ -0,0 +1,95 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +/// A `JsonConverter` that converts a nullable `String?` to a nullable `double?`. +/// +/// This converter is useful for converting JSON strings to Dart `double?` values and vice versa, +/// especially when dealing with APIs or data sources where numbers might be represented as strings. +/// +/// Example: +/// Assuming a JSON string of `{"valueIsDouble": "123.456"}` is returned from +/// the API and you want to access the value from your class as: +/// ```dart +/// final double? myValue = MyFreezedClass.valueIsDouble; +/// ``` +/// +/// You would implement the converter as follows: +/// +/// ```dart +/// @freezed +/// class MyFreezedClass with _$MyFreezedClass { +/// const factory MyFreezedClass({ +/// @DecimalConverter() double? valueIsDouble, +/// }) = _MyFreezedClass; +/// +/// factory MyFreezedClass.fromJson(Map json) => +/// _$MyFreezedClassFromJson(json); +/// +/// const MyFreezedClass._(); +/// } +/// ``` +class DoubleConverter implements JsonConverter { + /// Creates a const instance of [DoubleConverter]. + const DoubleConverter(); + + /// Converts a nullable `String?` to a nullable `double?`. + /// + /// If the input string is `null` or can't be parsed into a valid `double`, + /// this method returns `null`. + @override + double? fromJson(String? value) { + return double.tryParse(value ?? ""); + } + + /// Converts a nullable `double?` to a nullable `String?`. + /// + /// If the input `double` is `null`, this method returns `null`. + @override + String? toJson(double? value) => value?.toString(); +} + +/// A `JsonConverter` that converts a nullable `String?` to a nullable `int?`. +/// +/// This converter is useful for converting JSON strings to Dart `int?` values and vice versa, +/// especially when dealing with APIs or data sources where numbers might be represented as strings. +/// +/// Example: +/// Assuming a JSON string of `{"valueIsInt": "123"}` is returned from +/// the API and you want to access the value from your class as: +/// ```dart +/// final int? myValue = MyFreezedClass.valueIsInt; +/// ``` +/// +/// You would implement the converter as follows: +/// +/// ```dart +/// @freezed +/// class MyFreezedClass with _$MyFreezedClass { +/// const factory MyFreezedClass({ +/// @IntegerConverter() double? valueIsInt, +/// }) = _MyFreezedClass; +/// +/// factory MyFreezedClass.fromJson(Map json) => +/// _$MyFreezedClassFromJson(json); +/// +/// const MyFreezedClass._(); +/// } +/// ``` +class IntegerConverter implements JsonConverter { + /// Creates a const instance of [IntegerConverter]. + const IntegerConverter(); + + /// Converts a nullable `String?` to a nullable `int?`. + /// + /// If the input string is `null` or can't be parsed into a valid `int`, + /// this method returns `null`. + @override + int? fromJson(String? value) { + return int.tryParse(value ?? ""); + } + + /// Converts a nullable `int?` to a nullable `String?`. + /// + /// If the input `int` is `null`, this method returns `null`. + @override + String? toJson(int? value) => value?.toString(); +} diff --git a/lib/src/utils/ticker.dart b/lib/src/utils/ticker.dart new file mode 100644 index 0000000..a125412 --- /dev/null +++ b/lib/src/utils/ticker.dart @@ -0,0 +1,33 @@ +/// Creates a [Ticker] that emits a tick every [interval] seconds until the +/// [timeout] is reached. +/// +/// Options: +/// - `timeout` (Duration) - The amount of time the ticker should run for +/// - `interval` (Duration, _optional_) - The amount of time between each tick. +/// Defaults to `Duration(seconds: 1)`. +/// +/// Usage: +/// ```dart +/// final Stream ticker = const Ticker().tick( +/// timeout: Duration(seconds: 30), +/// interval: Duration(seconds: 5) +/// ); +/// +/// await for (final int ticksRemaining in ticker) { +/// if (ticksRemaining == 0) print("Time's up!"); +/// print('Tick! $ticksRemaining'); +/// } +/// ``` +/// +class Ticker { + const Ticker(); + + /// Starts the `Ticker`'s stream. + Stream tick({ + required Duration timeout, + Duration interval = const Duration(seconds: 1), + }) { + final int ticks = timeout.inSeconds ~/ interval.inSeconds; + return Stream.periodic(interval, (x) => ticks - x - 1).take(ticks); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..4d882a0 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,19 @@ +name: arcane_helper_utils +description: "A variety of utilities for Flutter and Dart" +version: 1.0.0 +homepage: https://github.com/hanskokx/arcane_helper_utils + +environment: + sdk: ^3.5.2 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + freezed_annotation: ^2.4.4 + week_number: ^1.1.0 + +dev_dependencies: + flutter_lints: ^4.0.0 + flutter_test: + sdk: flutter diff --git a/test/date_time_test.dart b/test/date_time_test.dart new file mode 100644 index 0000000..c958109 --- /dev/null +++ b/test/date_time_test.dart @@ -0,0 +1,97 @@ +import "package:arcane_helper_utils/src/extensions/date_time.dart"; +import "package:flutter_test/flutter_test.dart"; + +void main() { + group("[DateTime] StartAndEndOfPeriod", () { + test("Start of hour", () { + final DateTime startOfHour = DateTime(2000, 1, 1, 12, 36, 53).startOfHour; + expect(startOfHour, equals(DateTime(2000, 1, 1, 12, 0, 0))); + }); + + test("End of hour", () { + final DateTime endOfHour = DateTime(2000, 1, 1, 12, 36, 53).endOfHour; + expect(endOfHour, equals(DateTime(2000, 1, 1, 12, 59, 59, 999, 999))); + }); + + test("Start of day", () { + final DateTime startOfDay = DateTime(2000, 1, 1, 12, 36, 53).startOfDay; + expect(startOfDay, equals(DateTime(2000, 1, 1, 0, 0, 0))); + }); + + test("End of day", () { + final DateTime endOfDay = DateTime(2000, 1, 1, 12, 36, 53).endOfDay; + expect(endOfDay, equals(DateTime(2000, 1, 1, 23, 59, 59, 999, 999))); + }); + + test("Start of week", () { + final DateTime startOfWeek = DateTime(2000, 1, 1, 12, 36, 53).startOfWeek; + expect(startOfWeek, equals(DateTime(1999, 12, 27, 0, 0, 0))); + }); + + test("End of week", () { + final DateTime endOfWeek = DateTime(2000, 1, 1, 12, 36, 53).endOfWeek; + expect(endOfWeek, equals(DateTime(2000, 1, 2, 23, 59, 59, 999, 999))); + }); + + test("Start of month", () { + final DateTime startOfMonth = + DateTime(2000, 1, 17, 12, 36, 53).startOfMonth; + expect(startOfMonth, equals(DateTime(2000, 1, 1, 0, 0, 0))); + }); + + test("End of month", () { + final DateTime endOfMonth = DateTime(2000, 1, 17, 12, 36, 53).endOfMonth; + expect(endOfMonth, equals(DateTime(2000, 1, 31, 23, 59, 59, 999, 999))); + }); + + test("End of month (leap year)", () { + final DateTime endOfMonth = DateTime(2024, 2, 17, 12, 36, 53).endOfMonth; + expect(endOfMonth, equals(DateTime(2024, 2, 29, 23, 59, 59, 999, 999))); + }); + + test("Start of year", () { + final DateTime startOfYear = + DateTime(2000, 5, 17, 12, 36, 53).startOfYear; + expect(startOfYear, equals(DateTime(2000, 1, 1, 0, 0, 0))); + }); + + test("End of year", () { + final DateTime endOfYear = DateTime(2000, 4, 17, 12, 36, 53).endOfYear; + expect(endOfYear, equals(DateTime(2000, 12, 31, 23, 59, 59, 999, 999))); + }); + + test("End of year (leap year)", () { + final DateTime endOfYear = DateTime(2024, 4, 17, 12, 36, 53).endOfYear; + expect(endOfYear, equals(DateTime(2024, 12, 31, 23, 59, 59, 999, 999))); + }); + + test("First day of week", () { + final DateTime firstDayOfWeek = + DateTime(2024, 4, 17, 12, 36, 53).firstDayOfWeek; + expect( + firstDayOfWeek, + equals(DateTime(2024, 4, 15)), + ); + }); + }); + group("[DateTime] Calculations", () { + test("Is today", () { + final bool isToday = DateTime.now().isToday; + final bool isNotToday = + DateTime.now().subtract(const Duration(days: 1)).isToday; + expect(isToday, equals(true)); + expect(isNotToday, equals(false)); + }); + + test("Is same day as", () { + final DateTime first = DateTime(2000, 1, 1); + final DateTime second = DateTime(2000, 1, 2); + final DateTime third = DateTime(2000, 1, 1); + final bool firstAndSecond = first.isSameDayAs(second); + final bool firstAndThird = first.isSameDayAs(third); + + expect(firstAndSecond, equals(false)); + expect(firstAndThird, equals(true)); + }); + }); +}