diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml
new file mode 100644
index 0000000..8fc6a22
--- /dev/null
+++ b/.github/workflows/dart.yml
@@ -0,0 +1,43 @@
+name: Dart CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: dart-lang/setup-dart@v1
+ with:
+ sdk: main
+
+ - name: Install dependencies
+ run: dart pub get
+
+ - name: Check format
+ run: dart format --output=none --set-exit-if-changed -l 80 lib
+
+ - name: Analyze
+ run: dart analyze lib
+
+ - name: Run tests
+ run: dart test --coverage coverage --reporter=github
+
+ - name: Coverage
+ run: dart run coverage:format_coverage -l -i ./coverage/test/due_date_test.dart.vm.json -o ./coverage/lcov.info
+
+ - name: Upload coverage to codecov
+ uses: codecov/codecov-action@v3
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true # optional (default = false)
+ verbose: true # optional (default = false)
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..2e137cb
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,38 @@
+name: Update Docs
+
+on:
+ push:
+ branches:
+ - stable
+ paths:
+ - 'docs/api/**'
+ pull_request:
+ branches:
+ - stable
+ paths:
+ - 'docs/api/**'
+
+jobs:
+ update-docs:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Install git-filter-repo
+ run: sudo apt-get install -y git-filter-repo
+
+ - name: Switch to gh-pages and replace contents
+ run: |
+ git checkout gh-pages
+ git rm -r -f .
+ git checkout HEAD -- docs/api
+ mv docs/api/* .
+ git add .
+ git commit -m "Update gh-pages" || echo "No changes to commit"
+ git push
+
+ - name: Filter repo
+ run: |
+ git filter-repo --path docs/api
\ No newline at end of file
diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml
new file mode 100644
index 0000000..44546af
--- /dev/null
+++ b/.github/workflows/version.yml
@@ -0,0 +1,120 @@
+name: Update Version
+
+on:
+ push:
+ branches:
+ - stable
+ pull_request:
+ branches:
+ - stable
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up Dart
+ uses: dart-lang/setup-dart@v1
+
+ - name: Validate files
+ run: |
+ # Extract version from pubspec.yaml
+ version=$(grep 'version:' pubspec.yaml | cut -d ' ' -f 2)
+
+ # Get the version from the last commit on stable (if it exists)
+ git fetch origin stable:stable
+ last_version=$(git show stable:pubspec.yaml | grep 'version:' | cut -d ' ' -f 2)
+
+ # Check if the versions are different
+ if [ "$version" != "$last_version" ]; then
+ echo "Version has been updated to $version"
+ else
+ echo "Version has not been updated"
+ exit 1
+ fi
+
+ # Check if README.md and CHANGELOG.md contain the new version number
+ if grep -q "$version" README.md && grep -q "$version" CHANGELOG.md; then
+ echo "Version number found in README.md and CHANGELOG.md"
+ else
+ echo "Version number not found in README.md or CHANGELOG.md"
+ exit 1
+ fi
+
+ - name: Delete docs directory
+ run: rm -rf docs/*
+
+ - name: Generate docs
+ run: dart doc
+
+ - name: Commit changes
+ run: |
+ git add .
+ git commit -m "Update docs" || echo "No changes to commit"
+
+ - name: Push changes
+ run: git push
+
+ - name: Setup reviewdog
+ uses: reviewdog/action-setup@v1
+ with:
+ reviewdog_version: latest
+
+ - name: Generate tag message
+ id: generate_tag_message
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ VERSION_NUMBER=$(grep 'version:' pubspec.yaml | sed 's/version: //g')
+ LAST_VERSION=$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`)
+ COMMITS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ github.repository }}/compare/$LAST_VERSION...$VERSION_NUMBER)
+
+ WHATS_CHANGED=""
+ NEW_CONTRIBUTORS=""
+ CONTRIBUTORS=()
+
+ for row in $(echo "${COMMITS}" | jq -r '.commits[] | @base64'); do
+ _jq() {
+ echo ${row} | base64 --decode | jq -r ${1}
+ }
+
+ COMMIT_SHA=$(_jq '.sha')
+ COMMIT_MESSAGE=$(_jq '.commit.message')
+ AUTHOR_USERNAME=$(_jq '.author.login')
+ PR_NUMBER=$(curl -s -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ github.repository }}/commits/$COMMIT_SHA/pulls | jq -r '.[0].number')
+
+ WHATS_CHANGED+="$COMMIT_MESSAGE by @$AUTHOR_USERNAME in #$PR_NUMBER\n"
+
+ if [[ ! " ${CONTRIBUTORS[@]} " =~ " ${AUTHOR_USERNAME} " ]]; then
+ CONTRIBUTORS+=("$AUTHOR_USERNAME")
+ NEW_CONTRIBUTORS+="@$AUTHOR_USERNAME made their first contribution in #$PR_NUMBER\n"
+ fi
+ done
+
+ DESCRIPTION="What's Changed\n$WHATS_CHANGED\nNew Contributors\n$NEW_CONTRIBUTORS\nFull Changelog: $LAST_VERSION...$VERSION_NUMBER"
+ echo "::set-output name=description::$DESCRIPTION"
+
+ - name: Review tag message
+ run: |
+ echo "${{ steps.generate_tag_message.outputs.description }}" | reviewdog -f=diff -diff="git diff" -reporter=github-pr-review
+
+ - name: Create tag with version number
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ git tag -a $VERSION_NUMBER -m "${{ steps.generate_tag_message.outputs.description }}"
+ git push origin $VERSION_NUMBER
+
+ - name: Create draft release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ steps.generate_tag_message.outputs.version_number }}
+ release_name: Release ${{ steps.generate_tag_message.outputs.version_number }}
+ body: ${{ steps.generate_tag_message.outputs.description }}
+ draft: true
diff --git a/.hooks/pre-commit b/.hooks/pre-commit
new file mode 100644
index 0000000..c051c03
--- /dev/null
+++ b/.hooks/pre-commit
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+echo "Running pre-commit checks..."
+
+if git rev-parse --verify HEAD >/dev/null 2>&1
+then
+ against=HEAD
+else
+ # Initial commit: diff against an empty tree object
+ against=$(git hash-object -t tree /dev/null)
+fi
+
+# If you want to allow non-ASCII filenames set this variable to true.
+allownonascii=$(git config --type=bool hooks.allownonascii)
+
+# Redirect output to stderr.
+exec 1>&2
+
+# Cross platform projects tend to avoid non-ASCII filenames; prevent
+# them from being added to the repository. We exploit the fact that the
+# printable range starts at the space character and ends with tilde.
+if [ "$allownonascii" != "true" ] &&
+ # Note that the use of brackets around a tr range is ok here, (it's
+ # even required, for portability to Solaris 10's /usr/bin/tr), since
+ # the square bracket bytes happen to fall in the designated range.
+ test $(git diff --cached --name-only --diff-filter=A -z $against |
+ LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
+then
+ cat <<\EOF
+Error: Attempt to add a non-ASCII file name.
+
+This can cause problems if you want to work with people on other platforms.
+
+To be portable it is advisable to rename the file.
+
+If you know what you are doing you can disable this check using:
+
+ git config hooks.allownonascii true
+EOF
+ exit 1
+fi
+
+# Fetch packages
+echo "Running dart pub get..."
+dart pub get >/dev/null
+if [ $? -ne 0 ]; then
+ echo "dart pub get failed."
+ echo "Please run 'dart pub get' in the project root."
+ exit 1
+fi
+
+# Dart analyzer no errors should be found
+echo "Running Dart analyzer..."
+dart analyze
+if [ $? -ne 0 ]; then
+ echo "Dart analyzer found issues."
+ echo "Please run 'dart analyze' in the project root."
+ exit 1
+fi
+
+# Dart tests should pass
+echo "Running Dart tests..."
+dart test . --fail-fast 1>/dev/null
+# Print files that failed tests
+if [ $? -ne 0 ]; then
+ echo "Dart tests failed."
+ echo "Please run 'dart test' in the project root."
+ exit 1
+fi
+
+# Dart fix --apply then any changed files should be added to the commit
+echo "Running Dart fix..."
+dart fix --apply .
+git add .
+
+# Dart format then any changed files should be added to the commit
+echo "Running Dart format..."
+dart format .
+git add .
+
+# return 0 to indicate that everything went well
+echo "Pre-commit checks passed."
+exit 0
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..c97b9e9
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..639900d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 0000000..632ae1f
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,5 @@
+{
+ "default": true,
+ "MD013": false,
+ "MD033": false
+}
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..6654552
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "dart-code.dart-code",
+ "streetsidesoftware.code-spell-checker",
+ "davidanson.vscode-markdownlint"
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6324220..f1dbe44 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,7 +1,9 @@
{
"cSpell.words": [
+ "datetime",
"endtemplate",
"everies",
"overrider"
- ]
+ ],
+ "dart.lineLength": 80
}
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..5ed0a72
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,76 @@
+# Contributing to due_date
+
+First off, thanks for considering contributing to due_date! ❤️
+
+## Ways to Contribute
+
+Here are some ways you can contribute to the project:
+
+### Reporting Bugs
+
+If you find a bug, please file an issue describing the problem. Include steps to reproduce the bug so we can investigate.
+File issues at .
+
+### Suggesting Enhancements
+
+We welcome enhancement suggestions! File an issue explaining your idea with as much detail as possible.
+File issues at .
+
+### Contributing Code
+
+Code contributions are welcome! Please follow these guidelines:
+
+- Fork the repo and create a new branch for your change.
+
+- Run `dart analyze` to check for any static analysis issues.
+- Run `dart test` to run all tests. Make sure all tests pass.
+- Run `dart fix --apply` to fix any linting issues.
+- Run `dart format` to auto-format your code changes. Keep coding style consistent.
+- Consider writing tests covering your change. All code should have full test coverage.
+- Document any public API changes. Make sure docs are updated.
+- Submit a pull request with details explaining what you changed.
+Create a PR at
+
+We will review your PR and let you know if any other changes are needed.
+
+## Questions
+
+If you just have a question on using the library, ask away on our discussions tracker!
+Discuss related topics at .
+
+## Donations
+
+due_date is an open source project that I maintain in my free time. If you find it useful, donations are greatly appreciated!
+
+### Why Donate?
+
+Your donations allow me to spend more time maintaining and improving the package:
+
+- Responding to issues
+- Reviewing pull requests
+- Adding new features
+- Writing documentation
+- Updating dependencies
+
+Without donations, I need to limit my time spent based on what I can fit around other priorities in life.
+
+### Making a Donation
+
+If you're interested in donating to support due_date development, you can do so via:
+
+- [GitHub Sponsors](https://github.com/sponsors/fmorschel) - Recurring or one-time payments
+- [Buy me a coffee](https://www.buymeacoffee.com/fmorschel) - One-time payments
+
+All donations are greatly appreciated! Every bit helps justify more time spent on open source work.
+
+## Using Donations
+
+Donations help provide financial support but do not influence due_date's roadmap or feature priorities. Bug fixes and maintenance tasks always take priority.
+
+The majority of donations fund my time spent coding, documenting, reviewing, and responding to the community.
+
+I commit to being fully transparent about how donations are used to sustain development. Please feel free to ask questions!
+
+## License
+
+By contributing code, you agree to license your contribution under the [MIT license](LICENSE).
diff --git a/README.md b/README.md
index 85a53fa..d67a8f7 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,22 @@
+[![Build](https://github.com/fmorschel/due_date/actions/workflows/build.yml/badge.svg)](https://github.com/fmorschel/due_date/actions/workflows/build.yml)
+
+[![codecov](https://codecov.io/gh/fmorschel/due_date/branch/main/graph/badge.svg)](https://codecov.io/gh/fmorschel/due_date)
+
# DueDate
+[![pub package](https://img.shields.io/pub/v/due_date.svg)](https://pub.dev/packages/due_date)
+[![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/FMorschel/due_date/main/LICENSE)
+
+[![GitHub Sponsors](https://img.shields.io/github/sponsors/FMorschel)](https://github.com/sponsors/FMorschel)
+
+
+
+
A package for working with repeating `DateTime` patterns.
Ever wanted to create a new `DateTime` with, let's say, the same day in month? But the day is 31 and next month only has 30, so you go to 30 and the next day is lost because then you have no variable to save the original's month day? With `DueDateTime` this managing is done for you without any headaches.
@@ -90,6 +107,33 @@ See the API docs [here](https://fmorschel.github.io/due_date/).
Find more information at .
-Contibute to the package by creating a PR at .
+Contribute to the package by creating a PR at .
File issues at .
+
+Discuss related topics at .
+
+## Alternatives/Inspiration
+
+- [pub.dev -> time](https://pub.dev/packages/time)
+ - Made me start thinking about this package and how I could improve it with issues and discussions on the GitHub repo.
+
+- [DateUtils -> Flutter](https://api.flutter.dev/flutter/material/DateUtils-class.html)
+ - Inspired me to create the `Period` class and its methods. As well as some `Every` classes.
+
+## What makes this different
+
+This package is focused on working with `DateTime` objects and their patterns. It is not a calendar package, but it can be used to create one.
+It is also not a package to work with timezones. It is focused on the `DateTime` object itself. It does not have any timezone related methods. For that, you may want to check out the [timezone](https://pub.dev/packages/timezone) package.
+
+This is not a package to work with `Duration` objects. It is focused on `DateTime` objects and their patterns. For that, you may want to check out the [time](https://pub.dev/packages/time) package.
+
+This package is not a package to work with `DateTime` objects and their formatting. It is focused on `DateTime` objects and their patterns. For that, you may want to check out the [intl](https://pub.dev/packages/intl) package.
+
+This package is not intended to be a replacement for the `DateTime` class. It is intended to be a complement to it.
+
+The [time](https://pub.dev/packages/time) package is a great package to work with `Duration` and `DateTime` objects. It uses extension methods to add functionality to the `DateTime` class. This package is not intended to be a replacement for the `time` package. It is intended to be a complement to it.
+
+## License
+
+This package is licensed under the MIT license. See the [LICENSE]() file for details.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index a668208..2cfa6e0 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,4 +1,4 @@
-include: package:lints/recommended.yaml
+include: package:very_good_analysis/analysis_options.yaml
analyzer:
plugins:
@@ -68,6 +68,10 @@ dart_code_metrics:
linter:
rules:
+ omit_local_variable_types: false
+ avoid_equals_and_hash_code_on_mutable_classes: false
+ always_use_package_imports: false
+ public_member_api_docs: true
always_declare_return_types: true
always_put_required_named_parameters_first: true
always_require_non_null_named_parameters: true
diff --git a/lib/due_date.dart b/lib/due_date.dart
index f5df691..2195733 100644
--- a/lib/due_date.dart
+++ b/lib/due_date.dart
@@ -10,7 +10,7 @@
/// [WeekdayOccurrence]s constants) and [EveryDayInYear].
///
/// The [Every] class is a base class that processes all the base operations for
-/// [DueDateTime]. You can mix in one of the folowing mixins:
+/// [DueDateTime]. You can mix in one of the following mixins:
/// - [EveryWeek];
/// - [EveryMonth];
/// - [EveryYear];
@@ -23,16 +23,20 @@
/// class.
library due_date;
+import 'src/due_date.dart';
+import 'src/enums.dart' show WeekdayOccurrence;
+import 'src/every.dart';
+
export 'src/date_validator.dart';
export 'src/due_date.dart';
-export 'src/enums.dart' show Weekday, Month, Week, WeekdayOccurrence;
+export 'src/enums.dart' show Month, Week, Weekday, WeekdayOccurrence;
export 'src/every.dart';
export 'src/extensions.dart'
show
- DayInYear,
AddDays,
- WeekCalc,
ClampInMonth,
- PreviousNext,
DateValidatorListExt,
- EveryDateValidatorListExt;
+ DayInYear,
+ EveryDateValidatorListExt,
+ PreviousNext,
+ WeekCalc;
diff --git a/lib/fix_data/fix_date_validator.yaml b/lib/fix_data/fix_date_validator.yaml
new file mode 100644
index 0000000..2923017
--- /dev/null
+++ b/lib/fix_data/fix_date_validator.yaml
@@ -0,0 +1,134 @@
+
+version: 1
+transforms:
+ - title: "Use 'Weekday.occurrencesIn' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'occrurencesIn'
+ inClass: 'Weekday'
+ changes:
+ - kind: 'rename'
+ newName: 'occurrencesIn'
+
+ - title: "Use 'DateValidator.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidator'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorMixin.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorMixin'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorWeekday.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorWeekday'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorDueDayMonth.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorDueDayMonth'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorWeekdayCountInMonth.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorWeekdayCountInMonth'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorDayInYear.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorDayInYear'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorListMixin.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorListMixin'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorIntersection.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorIntersection'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorUnion.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorUnion'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorDifference.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorDifference'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
+
+ - title: "Use 'DateValidatorDifference.filterValidDates' instead."
+ date: 2023-11-28
+ bulkApply: true
+ element:
+ uris: ['package:due_date/due_date.dart', 'package:due_date/period.dart']
+ method: 'validsIn'
+ inClass: 'DateValidatorDifference'
+ changes:
+ - kind: 'rename'
+ newName: 'filterValidDates'
diff --git a/lib/period.dart b/lib/period.dart
index 7fc6bf7..1cbd130 100644
--- a/lib/period.dart
+++ b/lib/period.dart
@@ -16,7 +16,13 @@
/// [SemesterGenerator] and [YearGenerator].
library period;
-export 'src/enums.dart' show Weekday, Month, Week, PeriodGenerator;
+import 'src/enums.dart' show PeriodGenerator;
+import 'src/period.dart';
+import 'src/period_generator.dart';
+
+export 'src/constants.dart';
+export 'src/enums.dart' show Month, PeriodGenerator, Week, Weekday;
+export 'src/every_modifier.dart';
export 'src/extensions.dart' show EndOfDay;
export 'src/period.dart';
export 'src/period_generator.dart';
diff --git a/lib/src/constants.dart b/lib/src/constants.dart
new file mode 100644
index 0000000..c2ad3d0
--- /dev/null
+++ b/lib/src/constants.dart
@@ -0,0 +1,41 @@
+import '../due_date.dart';
+
+/// Simple class to delegate the work to a given [Every] base process.
+/// For every one one of the everies that is a [LimitedEvery], the limit
+/// will be passed.
+class LimitedOrEveryHandler {
+ const LimitedOrEveryHandler._();
+
+ /// Returns the start date considering the given [every] base process.
+ /// If [every] is a [LimitedEvery], the [limit] will be passed on.
+ static DateTime startDate(
+ T every,
+ DateTime date, {
+ required DateTime? limit,
+ }) {
+ if (every is! LimitedEvery) return every.startDate(date);
+ return every.startDate(date, limit: limit);
+ }
+
+ /// Returns the next date considering the given [every] base process.
+ /// If [every] is a [LimitedEvery], the [limit] will be passed on.
+ static DateTime next(
+ T every,
+ DateTime date, {
+ required DateTime? limit,
+ }) {
+ if (every is! LimitedEvery) return every.next(date);
+ return every.next(date, limit: limit);
+ }
+
+ /// Returns the previous date considering the given [every] base process.
+ /// If [every] is a [LimitedEvery], the [limit] will be passed on.
+ static DateTime previous(
+ T every,
+ DateTime date, {
+ required DateTime? limit,
+ }) {
+ if (every is! LimitedEvery) return every.previous(date);
+ return every.previous(date, limit: limit);
+ }
+}
diff --git a/lib/src/date_validator.dart b/lib/src/date_validator.dart
index 67f924a..372e3bc 100644
--- a/lib/src/date_validator.dart
+++ b/lib/src/date_validator.dart
@@ -10,16 +10,46 @@ abstract class DateValidator {
const DateValidator();
/// Returns true if the [date] is valid for this [DateValidator].
+ ///
+ /// This is the opposite of [valid].
+ /// Implementations that return true for invalid should also return false for
+ /// valid.
bool valid(DateTime date);
+ /// Returns true if the [date] is invalid for this [DateValidator].
+ ///
+ /// This is the opposite of [valid].
+ /// Implementations that return true for invalid should also return false for
+ /// valid.
+ ///
+ /// Usually, this will be implemented as `!valid(date)` by
+ /// [DateValidatorMixin]. However, if there is a simpler way to check
+ /// for invalid dates, it can be implemented here.
+ bool invalid(DateTime date);
+
+ @Deprecated("Use 'DateValidator.filterValidDates' instead.")
+
+ /// Returns the valid dates for this [DateValidator] in [dates].
+ Iterable validsIn(Iterable dates);
+
/// Returns the valid dates for this [DateValidator] in [dates].
- Iterable validsIn(List dates);
+ Iterable filterValidDates(Iterable dates);
}
-/// Mixin to easily implement the [DateValidator.validsIn] method.
+/// Mixin to easily implement the [DateValidator.invalid],
+/// [DateValidator.filterValidDates] and [DateValidator.filterValidDates]
+/// methods.
mixin DateValidatorMixin implements DateValidator {
@override
- Iterable validsIn(List dates) sync* {
+ bool invalid(DateTime date) => !valid(date);
+
+ @override
+ @Deprecated("Use 'DateValidator.filterValidDates' instead.")
+ Iterable validsIn(Iterable dates) =>
+ filterValidDates(dates);
+
+ @override
+ Iterable filterValidDates(Iterable dates) sync* {
for (final date in dates) {
if (valid(date)) yield date;
}
@@ -312,21 +342,32 @@ class DateValidatorUnion extends DelegatingList
/// one of the [validators].
class DateValidatorDifference extends DelegatingList
with EquatableMixin, DateValidatorMixin, DateValidatorListMixin {
- /// A [DateValidator] that validates a [DateTime] if the date is valid for only
- /// one of the [validators].
+ /// A [DateValidator] that validates a [DateTime] if the date is valid for
+ /// only one of the [validators].
const DateValidatorDifference(super.validators);
@override
bool valid(DateTime date) {
- int valids = 0;
+ int validCount = 0;
for (final validator in validators) {
- if (validator.valid(date)) valids++;
- if (valids > 1) return false;
+ if (validator.valid(date)) validCount++;
+ if (validCount > 1) return false;
}
- if (valids == 0) return false;
+ if (validCount == 0) return false;
return true;
}
+ @override
+ bool invalid(DateTime date) {
+ int validCount = 0;
+ for (final validator in validators) {
+ if (validator.valid(date)) validCount++;
+ if (validCount > 1) return true;
+ }
+ if (validCount == 0) return true;
+ return false;
+ }
+
@override
// ignore: hash_and_equals, already implemented by EquatableMixin
bool operator ==(Object other) {
diff --git a/lib/src/due_date.dart b/lib/src/due_date.dart
index 08af847..e7a684a 100644
--- a/lib/src/due_date.dart
+++ b/lib/src/due_date.dart
@@ -459,7 +459,7 @@ class DueDateTime extends DateTime with EquatableMixin {
/// Returns this DueDateTime value in the UTC time zone.
///
- /// Returns [this] if it is already in UTC.
+ /// Returns `this` if it is already in UTC.
/// Otherwise this method is equivalent to:
///
/// ```dart template:expression
@@ -474,7 +474,7 @@ class DueDateTime extends DateTime with EquatableMixin {
/// Returns this DueDateTime value in the local time zone.
///
- /// Returns [this] if it is already in the local time zone.
+ /// Returns `this` if it is already in the local time zone.
/// Otherwise this method is equivalent to:
///
/// ```dart template:expression
@@ -487,7 +487,7 @@ class DueDateTime extends DateTime with EquatableMixin {
date: super.toLocal(),
);
- /// Returns a new [DueDateTime] instance with [duration] added to [this].
+ /// Returns a new [DueDateTime] instance with [duration] added to `this`.
///
/// If [sameEvery] is true, keeps the current one.
/// If is false, the [every] will change to the [day] of the generated date.
@@ -501,9 +501,9 @@ class DueDateTime extends DateTime with EquatableMixin {
/// ```
///
/// Notice that the duration being added is actually 50 * 24 * 60 * 60
- /// seconds. If the resulting `DueDateTime` has a different daylight saving offset
- /// than `this`, then the result won't have the same time-of-day as `this`, and
- /// may not even hit the calendar date 50 days later.
+ /// seconds. If the resulting `DueDateTime` has a different daylight saving
+ /// offset than `this`, then the result won't have the same time-of-day as
+ /// `this`, and may not even hit the calendar date 50 days later.
///
/// Be careful when working with dates in local time.
@override
@@ -515,7 +515,8 @@ class DueDateTime extends DateTime with EquatableMixin {
return DueDateTime.fromDate(date, every: sameEvery ? every : null);
}
- /// Returns a new [DueDateTime] instance with [duration] subtracted from [this].
+ /// Returns a new [DueDateTime] instance with [duration] subtracted from
+ /// `this`.
///
/// If [sameEvery] is true, keeps the current one.
/// If is false, the [every] will change to the [day] of the generated date.
@@ -529,9 +530,9 @@ class DueDateTime extends DateTime with EquatableMixin {
/// ```
///
/// Notice that the duration being subtracted is actually 50 * 24 * 60 * 60
- /// seconds. If the resulting `DueDateTime` has a different daylight saving offset
- /// than `this`, then the result won't have the same time-of-day as `this`, and
- /// may not even hit the calendar date 50 days earlier.
+ /// seconds. If the resulting `DueDateTime` has a different daylight saving
+ /// offset than `this`, then the result won't have the same time-of-day as
+ /// `this`, and may not even hit the calendar date 50 days earlier.
///
/// Be careful when working with dates in local time.
@override
diff --git a/lib/src/enums.dart b/lib/src/enums.dart
index 0b3fd2c..f525480 100644
--- a/lib/src/enums.dart
+++ b/lib/src/enums.dart
@@ -5,28 +5,41 @@ import '../period.dart';
/// Weekday constants that are returned by [DateTime.weekday] method.
enum Weekday implements Comparable {
+ /// Monday.
monday(DateTime.monday, generator: WeekGenerator()),
+
+ /// Tuesday.
tuesday(
DateTime.tuesday,
generator: WeekGenerator(weekStart: DateTime.tuesday),
),
+
+ /// Wednesday.
wednesday(
DateTime.wednesday,
generator: WeekGenerator(weekStart: DateTime.wednesday),
),
+
+ /// Thursday.
thursday(
DateTime.thursday,
generator: WeekGenerator(weekStart: DateTime.thursday),
),
+
+ /// Friday.
friday(
DateTime.friday,
generator: WeekGenerator(weekStart: DateTime.friday),
),
+
+ /// Saturday.
saturday(
DateTime.saturday,
isWeekend: true,
generator: WeekGenerator(weekStart: DateTime.saturday),
),
+
+ /// Sunday.
sunday(
DateTime.sunday,
isWeekend: true,
@@ -77,16 +90,21 @@ enum Weekday implements Comparable {
/// Whether this weekday is a workday.
bool get isWorkday => !isWeekend;
- /// Returns the ammount of weekdays correspondent to this on the given [month]
+ /// Returns the amount of weekdays correspondent to this on the given [month]
+ /// of [year].
+ @Deprecated("Use 'Weekday.occurrencesIn' instead")
+ int occrurencesIn(int year, int month) => occurrencesIn(year, month);
+
+ /// Returns the amount of weekdays correspondent to this on the given [month]
/// of [year].
- int occrurencesIn(int year, int month) {
- DateTime date = DateTime.utc(year, month, 1);
+ int occurrencesIn(int year, int month) {
+ DateTime date = DateTime.utc(year, month);
int count = 0;
do {
if (date.weekday == dateTimeValue) {
count++;
}
- date = date.add(Duration(days: 1));
+ date = date.add(const Duration(days: 1));
} while (date.month == month);
return count;
}
@@ -146,8 +164,6 @@ enum Weekday implements Comparable {
return const EveryWeekday(saturday);
case sunday:
return const EveryWeekday(sunday);
- default:
- return EveryWeekday(this);
}
}
@@ -168,8 +184,6 @@ enum Weekday implements Comparable {
return const DateValidatorWeekday(saturday);
case sunday:
return const DateValidatorWeekday(sunday);
- default:
- return DateValidatorWeekday(this);
}
}
@@ -204,17 +218,40 @@ enum Weekday implements Comparable {
/// Month constants that are returned by [DateTime.month] method.
enum Month implements Comparable {
+ /// January month constant.
january(DateTime.january),
+
+ /// February month constant.
february(DateTime.february),
+
+ /// March month constant.
march(DateTime.march),
+
+ /// April month constant.
april(DateTime.april),
+
+ /// May month constant.
may(DateTime.may),
+
+ /// June month constant.
june(DateTime.june),
+
+ /// July month constant.
july(DateTime.july),
+
+ /// August month constant.
august(DateTime.august),
+
+ /// September month constant.
september(DateTime.september),
+
+ /// October month constant.
october(DateTime.october),
+
+ /// November month constant.
november(DateTime.november),
+
+ /// December month constant.
december(DateTime.december);
/// Month constants that are returned by [DateTime.month] method.
@@ -313,16 +350,25 @@ enum Month implements Comparable {
}
}
-/// Week occurences inside a month.
+/// Week occurrences inside a month.
///
/// The first week of the month is the one that contains the first day of the
/// month.
/// Sometimes the last week can be the same as the fourth.
enum Week implements Comparable {
+ /// First week.
first,
+
+ /// Second week.
second,
+
+ /// Third week.
third,
+
+ /// Fourth week.
fourth,
+
+ /// Last week.
last;
/// Returns the [Week] constant that corresponds to the given [date].
@@ -416,7 +462,7 @@ enum Week implements Comparable {
@override
int compareTo(Week other) => index.compareTo(other.index);
- /// Returns true if this is afrer [other].
+ /// Returns true if this is after [other].
bool operator >(Week other) => index > other.index;
/// Returns true if this is after or equal to [other].
@@ -466,210 +512,279 @@ enum Week implements Comparable {
enum WeekdayOccurrence
with DateValidatorMixin
implements EveryWeekdayCountInMonth {
+ /// The first Monday of the month.
firstMonday(
EveryWeekdayCountInMonth(
day: Weekday.monday,
week: Week.first,
),
),
+
+ /// The first Tuesday of the month.
firstTuesday(
EveryWeekdayCountInMonth(
day: Weekday.tuesday,
week: Week.first,
),
),
+
+ /// The first Wednesday of the month.
firstWednesday(
EveryWeekdayCountInMonth(
day: Weekday.wednesday,
week: Week.first,
),
),
+
+ /// The first Thursday of the month.
firstThursday(
EveryWeekdayCountInMonth(
day: Weekday.thursday,
week: Week.first,
),
),
+
+ /// The first Friday of the month.
firstFriday(
EveryWeekdayCountInMonth(
day: Weekday.friday,
week: Week.first,
),
),
+
+ /// The first Saturday of the month.
firstSaturday(
EveryWeekdayCountInMonth(
day: Weekday.saturday,
week: Week.first,
),
),
+
+ /// The first Sunday of the month.
firstSunday(
EveryWeekdayCountInMonth(
day: Weekday.sunday,
week: Week.first,
),
),
+
+ /// The second Monday of the month.
secondMonday(
EveryWeekdayCountInMonth(
day: Weekday.monday,
week: Week.second,
),
),
+
+ /// The second Tuesday of the month.
secondTuesday(
EveryWeekdayCountInMonth(
day: Weekday.tuesday,
week: Week.second,
),
),
+
+ /// The second Wednesday of the month.
secondWednesday(
EveryWeekdayCountInMonth(
day: Weekday.wednesday,
week: Week.second,
),
),
+
+ /// The second Thursday of the month.
secondThursday(
EveryWeekdayCountInMonth(
day: Weekday.thursday,
week: Week.second,
),
),
+
+ /// The second Friday of the month.
secondFriday(
EveryWeekdayCountInMonth(
day: Weekday.friday,
week: Week.second,
),
),
+
+ /// The second Saturday of the month.
secondSaturday(
EveryWeekdayCountInMonth(
day: Weekday.saturday,
week: Week.second,
),
),
+
+ /// The second Sunday of the month.
secondSunday(
EveryWeekdayCountInMonth(
day: Weekday.sunday,
week: Week.second,
),
),
+
+ /// The third Monday of the month.
thirdMonday(
EveryWeekdayCountInMonth(
day: Weekday.monday,
week: Week.third,
),
),
+
+ /// The third Tuesday of the month.
thirdTuesday(
EveryWeekdayCountInMonth(
day: Weekday.tuesday,
week: Week.third,
),
),
+
+ /// The third Wednesday of the month.
thirdWednesday(
EveryWeekdayCountInMonth(
day: Weekday.wednesday,
week: Week.third,
),
),
+
+ /// The third Thursday of the month.
thirdThursday(
EveryWeekdayCountInMonth(
day: Weekday.thursday,
week: Week.third,
),
),
+
+ /// The third Friday of the month.
thirdFriday(
EveryWeekdayCountInMonth(
day: Weekday.friday,
week: Week.third,
),
),
+
+ /// The third Saturday of the month.
thirdSaturday(
EveryWeekdayCountInMonth(
day: Weekday.saturday,
week: Week.third,
),
),
+
+ /// The third Sunday of the month.
thirdSunday(
EveryWeekdayCountInMonth(
day: Weekday.sunday,
week: Week.third,
),
),
+
+ /// The fourth Monday of the month.
fourthMonday(
EveryWeekdayCountInMonth(
day: Weekday.monday,
week: Week.fourth,
),
),
+
+ /// The fourth Tuesday of the month.
fourthTuesday(
EveryWeekdayCountInMonth(
day: Weekday.tuesday,
week: Week.fourth,
),
),
+
+ /// The fourth Wednesday of the month.
fourthWednesday(
EveryWeekdayCountInMonth(
day: Weekday.wednesday,
week: Week.fourth,
),
),
+
+ /// The fourth Thursday of the month.
fourthThursday(
EveryWeekdayCountInMonth(
day: Weekday.thursday,
week: Week.fourth,
),
),
+
+ /// The fourth Friday of the month.
fourthFriday(
EveryWeekdayCountInMonth(
day: Weekday.friday,
week: Week.fourth,
),
),
+
+ /// The fourth Saturday of the month.
fourthSaturday(
EveryWeekdayCountInMonth(
day: Weekday.saturday,
week: Week.fourth,
),
),
+
+ /// The fourth Sunday of the month.
fourthSunday(
EveryWeekdayCountInMonth(
day: Weekday.sunday,
week: Week.fourth,
),
),
+
+ /// The last Monday of the month.
lastMonday(
EveryWeekdayCountInMonth(
day: Weekday.monday,
week: Week.last,
),
),
+
+ /// The last Tuesday of the month.
lastTuesday(
EveryWeekdayCountInMonth(
day: Weekday.tuesday,
week: Week.last,
),
),
+
+ /// The last Wednesday of the month.
lastWednesday(
EveryWeekdayCountInMonth(
day: Weekday.wednesday,
week: Week.last,
),
),
+
+ /// The last Thursday of the month.
lastThursday(
EveryWeekdayCountInMonth(
day: Weekday.thursday,
week: Week.last,
),
),
+
+ /// The last Friday of the month.
lastFriday(
EveryWeekdayCountInMonth(
day: Weekday.friday,
week: Week.last,
),
),
+
+ /// The last Saturday of the month.
lastSaturday(
EveryWeekdayCountInMonth(
day: Weekday.saturday,
week: Week.last,
),
),
+
+ /// The last Sunday of the month.
lastSunday(
EveryWeekdayCountInMonth(
day: Weekday.sunday,
diff --git a/lib/src/every.dart b/lib/src/every.dart
index 206b567..efa625a 100644
--- a/lib/src/every.dart
+++ b/lib/src/every.dart
@@ -1,5 +1,6 @@
import 'package:time/time.dart';
+import 'constants.dart';
import 'date_validator.dart';
import 'enums.dart';
import 'extensions.dart';
@@ -11,7 +12,8 @@ import 'extensions.dart';
///
/// See [EveryWeek], [EveryMonth], [EveryYear] for your base implementations.
abstract class Every {
- /// Abstract class that, when extended, processes [DateTime] with custom logic.
+ /// Abstract class that, when extended, processes [DateTime] with custom
+ /// logic.
///
/// See [EveryWeekday], [EveryDueDayMonth], [EveryWeekdayCountInMonth] (also
/// [WeekdayOccurrence]) and [EveryDayInYear] for complete base
@@ -20,41 +22,67 @@ abstract class Every {
/// See [EveryWeek], [EveryMonth], [EveryYear] for your base implementations.
const Every();
+ /// {@template startDate}
/// Returns the next [DateTime] that matches the [Every] pattern.
+ ///
+ /// If the [date] is a [DateTime] that matches the [Every] pattern, the
+ /// [DateTime] will be returned.
+ /// {@endtemplate}
DateTime startDate(DateTime date);
+ /// {@template next}
/// Returns the next instance of the given [date] considering this [Every]
/// base process.
+ ///
+ /// If the [date] is a [DateTime] that matches the [Every] pattern, a new
+ /// [DateTime] will be generated.
+ /// {@endtemplate}
DateTime next(DateTime date);
+ /// {@template previous}
/// Returns the previous instance of the given [date] considering this [Every]
/// base process.
+ ///
+ /// If the [date] is a [DateTime] that matches the [Every] pattern, a new
+ /// [DateTime] will be generated.
+ /// {@endtemplate}
DateTime previous(DateTime date);
}
/// Abstract class that forces the implementation of [Every] to have a
/// limit parameter for the [startDate], [next] and [previous] methods.
abstract class LimitedEvery extends Every {
- /// Abstract class that, when extended, processes [DateTime] with custom logic.
+ /// Abstract class that, when extended, processes [DateTime] with custom
+ /// logic.
///
- /// See [EveryWeekday], [EveryDueDayMonth], [EveryWeekdayCountInMonth] (also
- /// [WeekdayOccurrence]) and [EveryDayInYear] for complete base
- /// implementations.
+ /// Abstract class that forces the implementation of [Every] to have a
+ /// limit parameter for the [startDate], [next] and [previous] methods.
+ ///
+ /// See [EveryDateValidatorDifference], [EveryDateValidatorIntersection] and
+ /// [EveryDateValidatorUnion] for complete base implementations.
///
/// See [EveryWeek], [EveryMonth], [EveryYear] for your base implementations.
const LimitedEvery();
- /// Returns the next [DateTime] that matches the [Every] pattern.
+ /// {@macro startDate}
+ ///
+ /// {@template limit}
+ /// If the generated [DateTime] is still not able to return the first call to
+ /// this function and it has passed the [limit], it will throw a
+ /// [DateTimeLimitReachedException].
+ /// {@endtemplate}
@override
DateTime startDate(DateTime date, {DateTime? limit});
- /// Returns the next instance of the given [date] considering this [Every]
- /// base process.
+ /// {@macro next}
+ ///
+ /// {@macro limit}
@override
DateTime next(DateTime date, {DateTime? limit});
- /// Returns the previous instance of the given [date] considering this [Every]
- /// base process.
+ /// {@macro previous}
+ ///
+ /// {@macro limit}
@override
DateTime previous(DateTime date, {DateTime? limit});
}
@@ -182,16 +210,35 @@ class EveryWeekday extends DateValidatorWeekday
/// Returns the next date that fits the [weekday].
@override
DateTime startDate(DateTime date) {
- final day = weekday.fromWeekOf(date);
- if (day.toUtc().date.isBefore(date.toUtc().date)) {
- final nextDay = day.toUtc().add(const Duration(days: 1));
- if (date.isUtc) {
- return nextDay.nextWeekday(weekday);
- } else {
- return nextDay.toLocal().nextWeekday(weekday);
- }
+ if (valid(date)) return date;
+ return next(date);
+ }
+
+ /// Returns the previous date that fits the [weekday].
+ ///
+ /// Always returns the first [DateTime] that fits the [weekday], ignoring
+ /// the given [date] as an option.
+ @override
+ DateTime next(DateTime date) {
+ if (date.weekday < weekday.dateTimeValue) {
+ return weekday.fromWeekOf(date);
} else {
- return day;
+ return weekday
+ .fromWeekOf(date.lastDayOfWeek.add(const Duration(days: 1)));
+ }
+ }
+
+ /// Returns the previous date that fits the [weekday].
+ ///
+ /// Always returns the last [DateTime] that fits the [weekday], ignoring
+ /// the given [date] as an option.
+ @override
+ DateTime previous(DateTime date) {
+ if (date.weekday > weekday.dateTimeValue) {
+ return weekday.fromWeekOf(date);
+ } else {
+ return weekday
+ .fromWeekOf(date.firstDayOfWeek.subtract(const Duration(days: 1)));
}
}
@@ -200,23 +247,26 @@ class EveryWeekday extends DateValidatorWeekday
@override
DateTime addWeeks(DateTime date, int weeks) {
if (weeks == 0) return date;
- if (!valid(date)) {
- if (weeks.isNegative) {
- if (date.weekday < weekday.dateTimeValue) {
- date = date.firstDayOfWeek.subtract(const Duration(days: 1));
+ int localWeeks = weeks;
+ DateTime localDate = date.copyWith();
+ if (!valid(localDate)) {
+ if (localWeeks.isNegative) {
+ if (localDate.weekday < weekday.dateTimeValue) {
+ localDate =
+ localDate.firstDayOfWeek.subtract(const Duration(days: 1));
}
- date = weekday.fromWeekOf(date);
- weeks++;
+ localDate = weekday.fromWeekOf(localDate);
+ localWeeks++;
} else {
- if (date.weekday > weekday.dateTimeValue) {
- date = date.lastDayOfWeek.add(const Duration(days: 1));
+ if (localDate.weekday > weekday.dateTimeValue) {
+ localDate = localDate.lastDayOfWeek.add(const Duration(days: 1));
}
- date = weekday.fromWeekOf(date);
- weeks--;
+ localDate = weekday.fromWeekOf(localDate);
+ localWeeks--;
}
}
- final day = date.toUtc().add(Duration(days: weeks * 7));
- return _solveFor(date, day);
+ final day = localDate.toUtc().add(Duration(days: localWeeks * 7));
+ return _solveFor(localDate, day);
}
/// Returns a new [DateTime] where the week is the same([Week]) inside the
@@ -297,22 +347,26 @@ class EveryDueDayMonth extends DateValidatorDueDayMonth
@override
DateTime addMonths(DateTime date, int months) {
if (months == 0) return date;
- if (!valid(date)) {
- if (months.isNegative) {
- if (date.day < dueDay) {
- date = date.firstDayOfMonth.subtract(const Duration(days: 1));
+ int localMonths = months;
+ DateTime localDate = date.copyWith();
+ if (!valid(localDate)) {
+ if (localMonths.isNegative) {
+ if (localDate.day < dueDay) {
+ localDate =
+ localDate.firstDayOfMonth.subtract(const Duration(days: 1));
}
- date = _thisMonthsDay(date);
- months++;
+ localDate = _thisMonthsDay(localDate);
+ localMonths++;
} else {
- if (date.day > dueDay) {
- date = date.lastDayOfMonth.add(const Duration(days: 1));
+ if (localDate.day > dueDay) {
+ localDate = localDate.lastDayOfMonth.add(const Duration(days: 1));
}
- date = _thisMonthsDay(date);
- months--;
+ localDate = _thisMonthsDay(localDate);
+ localMonths--;
}
}
- final day = date.copyWith(month: date.month + months, day: 1);
+ final day =
+ localDate.copyWith(month: localDate.month + localMonths, day: 1);
return day.copyWith(day: dueDay).clamp(max: day.lastDayOfMonth);
}
@@ -349,7 +403,7 @@ class EveryDueDayMonth extends DateValidatorDueDayMonth
}
/// Class that processes [DateTime] so that the [addMonths] always returns the
-/// next month's with the [week] occurence of the [day] ([DateTime.weekday]
+/// next month's with the [week] occurrence of the [day] ([DateTime.weekday]
/// is the [day]'s [Weekday.dateTimeValue]).
///
/// E.g.
@@ -381,15 +435,34 @@ class EveryWeekdayCountInMonth extends DateValidatorWeekdayCountInMonth
/// - If the current [date] - [DateTime.day] is less than the [DateTime.month]
/// [week], it's returned the same month with the [DateTime.day] being the
/// [day] of the [week].
- /// - If the current [date] - [DateTime.day] is greater than the [DateTime.month]
- /// [week], it's returned the next month with the [DateTime.day] being the
- /// [day] of the [week].
+ /// - If the current [date] - [DateTime.day] is greater than the
+ /// [DateTime.month], [week], it's returned the next month with the
+ /// [DateTime.day] being the [day] of the [week].
@override
DateTime startDate(DateTime date) {
if (valid(date)) return date;
final thisMonthDay = week.weekdayOf(
year: date.year,
- month: date.day,
+ month: date.month,
+ day: day,
+ utc: date.isUtc,
+ );
+ if (date.day < thisMonthDay.day) return thisMonthDay;
+ return next(date);
+ }
+
+ /// Returns the next date that fits the [day] and the [week].
+ /// - If the current [date] - [DateTime.day] is less than the [DateTime.month]
+ /// [week], it's returned the same month with the [DateTime.day] being the
+ /// [day] of the [week].
+ /// - If the current [date] - [DateTime.day] is greater than the
+ /// [DateTime.month], [week], it's returned the next month with the
+ /// [DateTime.day] being the [day] of the [week].
+ @override
+ DateTime next(DateTime date) {
+ final thisMonthDay = week.weekdayOf(
+ year: date.year,
+ month: date.month,
day: day,
utc: date.isUtc,
);
@@ -402,41 +475,52 @@ class EveryWeekdayCountInMonth extends DateValidatorWeekdayCountInMonth
);
}
- /// Returns the [date] - [DateTime.month] + [months] with the [week] occurence
- /// of the [day].
+ /// Returns the previous date that fits the [day] and the [week].
+ /// - If the current [date] - [DateTime.day] is less than the [DateTime.month]
+ /// [week], it's returned the same month with the [DateTime.day] being the
+ /// [day] of the [week].
+ /// - If the current [date] - [DateTime.day] is greater than the
+ /// [DateTime.month], [week], it's returned the previous month with the
+ /// [DateTime.day] being the [day] of the [week].
+ @override
+ DateTime previous(DateTime date) {
+ final thisMonthDay = week.weekdayOf(
+ year: date.year,
+ month: date.month,
+ day: day,
+ utc: date.isUtc,
+ );
+ if (date.day > thisMonthDay.day) return thisMonthDay;
+ return week.weekdayOf(
+ year: date.year,
+ month: date.month - 1,
+ day: day,
+ utc: date.isUtc,
+ );
+ }
+
+ /// Returns the [date] - [DateTime.month] + [months] with the [week]
+ /// occurrence of the [day].
@override
DateTime addMonths(DateTime date, int months) {
- if (months == 0) return date;
- if (!valid(date)) {
- final thisMonthsDay = week.weekdayOf(
- year: date.year,
- month: date.month,
- day: day,
- utc: date.isUtc,
- );
- if (months.isNegative) {
- if (date.day < thisMonthsDay.day) {
- date = date.firstDayOfMonth.subtract(const Duration(days: 1));
- months++;
- }
- } else {
- if (date.day > thisMonthsDay.day) {
- date = date.lastDayOfMonth.add(const Duration(days: 1));
- months--;
- }
+ if (months == 0) return startDate(date);
+ int localMonths = months;
+ DateTime localDate = startDate(date);
+ if (localMonths.isNegative) {
+ while (localMonths < 0) {
+ localDate = previous(localDate);
+ localMonths++;
+ }
+ } else {
+ while (localMonths > 0) {
+ localDate = next(localDate);
+ localMonths--;
}
}
- return week
- .weekdayOf(
- year: date.year,
- month: date.month + months,
- day: day,
- utc: date.isUtc,
- )
- .add(date.timeOfDay);
+ return localDate;
}
- /// Returns the [date] - [DateTime.year] + [years] with the [week] occurence
+ /// Returns the [date] - [DateTime.year] + [years] with the [week] occurrence
/// of the [day].
///
/// Basically, it's the same as [addMonths] but with the months parameter
@@ -480,6 +564,14 @@ class EveryDayInYear extends DateValidatorDayInYear
}
/// Returns the next date that fits the [dayInYear].
+ /// - If the current [date] - [DayInYear.dayInYear] is equal to `zero`,
+ /// [date] is returned.
+ /// - If the current [date] - [DayInYear.dayInYear] is less than the
+ /// [dayInYear], it's returned the same year with the [DayInYear.dayInYear]
+ /// being the [dayInYear].
+ /// - If the current [date] - [DayInYear.dayInYear] is greater than the
+ /// [dayInYear], it's returned the next year with the [DayInYear.dayInYear]
+ /// being the [dayInYear].
@override
DateTime startDate(DateTime date) {
if (valid(date)) return date;
@@ -487,36 +579,71 @@ class EveryDayInYear extends DateValidatorDayInYear
.add(Duration(days: dayInYear - 1))
.clamp(max: date.lastDayOfYear);
if (date.dayInYear <= dayInYear) return thisYearDay;
- return date.lastDayOfYear
- .add(Duration(days: dayInYear))
+ return next(date);
+ }
+
+ /// Returns the next date that fits the [dayInYear].
+ /// - If the current [date] - [DayInYear.dayInYear] is less than the
+ /// [dayInYear], it's returned the same year with the [DayInYear.dayInYear]
+ /// being the [dayInYear].
+ /// - If the current [date] - [DayInYear.dayInYear] is greater than the
+ /// [dayInYear], it's returned the next year with the [DayInYear.dayInYear]
+ /// being the [dayInYear].
+ @override
+ DateTime next(DateTime date) {
+ if (!date.isLeapYear && dayInYear == 366) {
+ return date.copyWith(year: date.year + 1).lastDayOfYear;
+ }
+ final thisYearDay = date.firstDayOfYear
+ .add(Duration(days: dayInYear - 1))
+ .clamp(max: date.lastDayOfYear);
+ if (date.dayInYear < dayInYear) return thisYearDay;
+ return date
+ .copyWith(year: date.year + 1)
+ .firstDayOfYear
+ .add(Duration(days: dayInYear - 1))
.clamp(max: date.copyWith(year: date.year + 1).lastDayOfYear);
}
+ /// Returns the previous date that fits the [dayInYear].
+ /// - If the current [date] - [DayInYear.dayInYear] is greater than the
+ /// [dayInYear], it's returned the same year with the [DayInYear.dayInYear]
+ /// being the [dayInYear].
+ /// - If the current [date] - [DayInYear.dayInYear] is less than the
+ /// [dayInYear], it's returned the previous year with the
+ /// [DayInYear.dayInYear] being the [dayInYear].
+ @override
+ DateTime previous(DateTime date) {
+ final thisYearDay = date.firstDayOfYear
+ .add(Duration(days: dayInYear - 1))
+ .clamp(max: date.lastDayOfYear);
+ if (date.dayInYear > dayInYear) return thisYearDay;
+ return date
+ .copyWith(year: date.year - 1)
+ .firstDayOfYear
+ .add(Duration(days: dayInYear - 1))
+ .clamp(max: date.copyWith(year: date.year - 1).lastDayOfYear);
+ }
+
/// Returns a new [DateTime] where the year is [years] from this year and the
/// [DateTime.day] is equal to [dayInYear]-1 added to January 1st.
@override
DateTime addYears(DateTime date, int years) {
- if (years == 0) return date;
- if (!valid(date)) {
- final thisYearsDay = date.firstDayOfYear
- .add(Duration(days: dayInYear - 1))
- .clamp(max: date.lastDayOfYear);
- if (years.isNegative) {
- if (date.day < thisYearsDay.day) {
- date = date.firstDayOfYear.subtract(const Duration(days: 1));
- years++;
- }
- } else {
- if (date.day > thisYearsDay.day) {
- date = date.lastDayOfYear.add(const Duration(days: 1));
- years--;
- }
+ if (years == 0) return startDate(date);
+ int localYears = years;
+ DateTime localDate = startDate(date);
+ if (localYears.isNegative) {
+ while (localYears < 0) {
+ localDate = previous(localDate);
+ localYears++;
+ }
+ } else {
+ while (localYears > 0) {
+ localDate = next(localDate);
+ localYears--;
}
}
- return date.firstDayOfYear
- .copyWith(year: date.year + years)
- .add(Duration(days: dayInYear - 1))
- .clamp(max: date.copyWith(year: date.year + years).lastDayOfYear);
+ return localDate;
}
@override
@@ -548,7 +675,9 @@ class EveryDateValidatorIntersection
if ((limit != null) && (date.isAfter(limit) || (date == limit))) {
throw DateTimeLimitReachedException(date: date, limit: limit);
}
- final startingDates = map((every) => _startDate(every, date, limit: limit));
+ final startingDates = map(
+ (every) => LimitedOrEveryHandler.startDate(every, date, limit: limit),
+ );
final validDates = startingDates.where(valid);
if (validDates.isNotEmpty) {
final result = validDates.reduce(_reduceFuture);
@@ -566,7 +695,8 @@ class EveryDateValidatorIntersection
if ((limit != null) && (date.isAfter(limit) || (date == limit))) {
throw DateTimeLimitReachedException(date: date, limit: limit);
}
- final nextDates = map((every) => _next(every, date, limit: limit));
+ final nextDates =
+ map((every) => LimitedOrEveryHandler.next(every, date, limit: limit));
final validDates = nextDates.where(valid);
if (validDates.isNotEmpty) {
final result = validDates.reduce(_reduceFuture);
@@ -584,7 +714,9 @@ class EveryDateValidatorIntersection
if ((limit != null) && (date.isBefore(limit) || (date == limit))) {
throw DateTimeLimitReachedException(date: date, limit: limit);
}
- final previousDates = map((every) => _previous(every, date, limit: limit));
+ final previousDates = map(
+ (every) => LimitedOrEveryHandler.previous(every, date, limit: limit),
+ );
final validDates = previousDates.where(valid);
if (validDates.isNotEmpty) {
final result = validDates.reduce(_reducePast);
@@ -623,7 +755,9 @@ class EveryDateValidatorUnion
@override
DateTime startDate(DateTime date, {DateTime? limit}) {
if (isEmpty) return date;
- final startingDates = map((every) => _startDate(every, date, limit: limit));
+ final startingDates = map(
+ (every) => LimitedOrEveryHandler.startDate(every, date, limit: limit),
+ );
return startingDates.reduce(_reduceFuture);
}
@@ -636,7 +770,8 @@ class EveryDateValidatorUnion
@override
DateTime next(DateTime date, {DateTime? limit}) {
if (isEmpty) return date;
- final nextDates = map((every) => _next(every, date, limit: limit));
+ final nextDates =
+ map((every) => LimitedOrEveryHandler.next(every, date, limit: limit));
return nextDates.reduce(_reduceFuture);
}
@@ -649,7 +784,9 @@ class EveryDateValidatorUnion
@override
DateTime previous(DateTime date, {DateTime? limit}) {
if (isEmpty) return date;
- final previousDates = map((every) => _previous(every, date, limit: limit));
+ final previousDates = map(
+ (every) => LimitedOrEveryHandler.previous(every, date, limit: limit),
+ );
return previousDates.reduce(_reducePast);
}
@@ -678,7 +815,9 @@ class EveryDateValidatorDifference
if ((limit != null) && (date.isAfter(limit) || (date == limit))) {
throw DateTimeLimitReachedException(date: date, limit: limit);
}
- final startingDates = map((every) => _startDate(every, date, limit: limit));
+ final startingDates = map(
+ (every) => LimitedOrEveryHandler.startDate(every, date, limit: limit),
+ );
final validDates = startingDates.where(valid);
if (validDates.isNotEmpty) {
final result = validDates.reduce(_reduceFuture);
@@ -696,7 +835,8 @@ class EveryDateValidatorDifference
if ((limit != null) && (date.isAfter(limit) || (date == limit))) {
throw DateTimeLimitReachedException(date: date, limit: limit);
}
- final nextDates = map((every) => _next(every, date, limit: limit));
+ final nextDates =
+ map((every) => LimitedOrEveryHandler.next(every, date, limit: limit));
final validDates = nextDates.where(valid);
if (validDates.isNotEmpty) {
final result = validDates.reduce(_reduceFuture);
@@ -714,7 +854,9 @@ class EveryDateValidatorDifference
if ((limit != null) && (date.isBefore(limit) || (date == limit))) {
throw DateTimeLimitReachedException(date: date, limit: limit);
}
- final previousDates = map((every) => _previous(every, date, limit: limit));
+ final previousDates = map(
+ (every) => LimitedOrEveryHandler.previous(every, date, limit: limit),
+ );
final validDates = previousDates.where(valid);
if (validDates.isNotEmpty) {
final result = validDates.reduce(_reducePast);
@@ -735,48 +877,19 @@ class EveryDateValidatorDifference
}
}
-DateTime _startDate(
- T every,
- DateTime date, {
- required DateTime? limit,
-}) {
- if (every is! LimitedEvery) return every.startDate(date);
- return every.startDate(date, limit: limit);
-}
-
-DateTime _next(
- T every,
- DateTime date, {
- required DateTime? limit,
-}) {
- if (every is! LimitedEvery) return every.next(date);
- return every.next(date, limit: limit);
-}
-
-DateTime _previous(
- T every,
- DateTime date, {
- required DateTime? limit,
-}) {
- if (every is! LimitedEvery) return every.previous(date);
- return every.previous(date, limit: limit);
-}
-
-DateTime _reduceFuture(DateTime value, DateTime element) {
- return value.isBefore(element) ? value : element;
-}
-
-DateTime _reducePast(DateTime value, DateTime element) {
- return value.isAfter(element) ? value : element;
-}
-
/// Exception thrown when a date limit is reached.
+///
+/// Thrown when a [LimitedEvery] method would return a date that is after (or
+/// before in [LimitedEvery.previous] case) the [limit] date.
+///
+/// Should **_not_** be thrown when the resulting [date] is equal to the [limit]
+/// date.
class DateTimeLimitReachedException implements Exception {
/// Exception thrown when a date limit is reached.
const DateTimeLimitReachedException({
required this.date,
required this.limit,
- });
+ }) : assert(date != limit, 'Invalid exception');
/// Date that reached the limit.
final DateTime date;
@@ -789,3 +902,11 @@ class DateTimeLimitReachedException implements Exception {
return 'DateTimeLimitException: $date has passed $limit';
}
}
+
+DateTime _reduceFuture(DateTime value, DateTime element) {
+ return value.isBefore(element) ? value : element;
+}
+
+DateTime _reducePast(DateTime value, DateTime element) {
+ return value.isAfter(element) ? value : element;
+}
diff --git a/lib/src/every_modifier.dart b/lib/src/every_modifier.dart
new file mode 100644
index 0000000..d84bf4e
--- /dev/null
+++ b/lib/src/every_modifier.dart
@@ -0,0 +1,483 @@
+import 'package:equatable/equatable.dart';
+
+import '../due_date.dart';
+import 'constants.dart';
+
+/// An enum that represents the direction of the process inside [EveryModifier].
+/// Used on [EveryModifier.processDate].
+enum DateDirection {
+ /// An enum that represents the start direction of the process inside
+ /// [EveryModifier].
+ start,
+
+ /// An enum that represents the next direction of the process inside
+ /// [EveryModifier].
+ next,
+
+ /// An enum that represents the previous direction of the process inside
+ /// [EveryModifier].
+ previous;
+
+ /// Returns true if the [DateDirection] is [DateDirection.start].
+ bool get isStart => this == DateDirection.start;
+
+ /// Returns true if the [DateDirection] is [DateDirection.next].
+ bool get isNext => this == DateDirection.next;
+
+ /// Returns true if the [DateDirection] is [DateDirection.previous].
+ bool get isPrevious => this == DateDirection.previous;
+}
+
+/// {@template everyModifier}
+/// Abstract class that, when extended, processes [DateTime] with custom logic.
+/// {@endtemplate}
+abstract class EveryModifier implements Every {
+ /// {@macro everyModifier}
+ const EveryModifier({
+ required this.every,
+ });
+
+ /// The base generator for this [EveryModifier].
+ final T every;
+
+ /// A method that processes [date] with custom logic.
+ DateTime processDate(DateTime date, DateDirection direction);
+}
+
+/// {@template everyModifierMixin}
+/// Mixin that, when used, passes the calls the specific method on the
+/// underlying [every].
+///
+/// If the [every] is a [LimitedEvery], the [LimitedEveryModifierMixin] should
+/// be used instead.
+/// {@endtemplate}
+mixin EveryModifierMixin on EveryModifier {
+ @override
+ DateTime startDate(DateTime date) {
+ return processDate(
+ LimitedOrEveryHandler.startDate(every, date, limit: null),
+ DateDirection.start,
+ );
+ }
+
+ @override
+ DateTime next(DateTime date) {
+ return processDate(
+ LimitedOrEveryHandler.next(every, date, limit: null),
+ DateDirection.next,
+ );
+ }
+
+ @override
+ DateTime previous(DateTime date) {
+ return processDate(
+ LimitedOrEveryHandler.previous(every, date, limit: null),
+ DateDirection.previous,
+ );
+ }
+}
+
+/// {@macro everyModifierMixin}
+///
+/// Also makes the using class a [LimitedEvery].
+///
+/// Should **always** be used when the [every] is a [LimitedEvery].
+mixin LimitedEveryModifierMixin on EveryModifier
+ implements LimitedEvery {
+ @override
+ DateTime startDate(DateTime date, {DateTime? limit}) {
+ return processDate(
+ LimitedOrEveryHandler.startDate(every, date, limit: limit),
+ DateDirection.start,
+ limit: limit,
+ );
+ }
+
+ @override
+ DateTime next(DateTime date, {DateTime? limit}) {
+ return processDate(
+ LimitedOrEveryHandler.next(every, date, limit: limit),
+ DateDirection.next,
+ limit: limit,
+ );
+ }
+
+ @override
+ DateTime previous(DateTime date, {DateTime? limit}) {
+ return processDate(
+ LimitedOrEveryHandler.previous(every, date, limit: limit),
+ DateDirection.previous,
+ limit: limit,
+ );
+ }
+
+ @override
+ DateTime processDate(
+ DateTime date,
+ DateDirection direction, {
+ DateTime? limit,
+ });
+}
+
+/// {@template everyModifierInvalidator}
+/// Class that wraps an [every] generator and adds an [invalidator] that will
+/// be used to invalidate the generated dates.
+/// {@endtemplate}
+abstract class EveryModifierInvalidator
+ extends EveryModifier with EveryModifierMixin {
+ /// {@macro everyModifierInvalidator}
+ const EveryModifierInvalidator({
+ required super.every,
+ required this.invalidator,
+ });
+
+ /// The [DateValidator] that will be used to invalidate the generated dates.
+ final DateValidator invalidator;
+}
+
+/// {@template everySkipInvalidModifier}
+/// Class that wraps an [Every] generator and adds a [DateValidator] that will
+/// be used to invalidate the generated dates.
+///
+/// It will return the next [DateTime] that matches the [every] pattern and is
+/// not valid for the [invalidator].
+/// {@endtemplate}
+class EverySkipInvalidModifier
+ extends EveryModifierInvalidator
+ with EquatableMixin, DateValidatorMixin, LimitedEveryModifierMixin
+ implements EveryDateValidator {
+ /// {@macro everySkipInvalidModifier}
+ const EverySkipInvalidModifier({
+ required super.every,
+ required super.invalidator,
+ });
+
+ /// Returns the next [DateTime] that matches the [every] pattern and is not
+ /// valid for the [invalidator].
+ @override
+ DateTime startDate(DateTime date, {DateTime? limit}) =>
+ super.startDate(date, limit: limit);
+
+ /// Returns the next instance of the given [date] considering the [every]
+ /// base process. If the [date] is valid for the [invalidator], a new
+ /// [DateTime] will be returned.
+ @override
+ DateTime next(DateTime date, {DateTime? limit}) =>
+ super.next(date, limit: limit);
+
+ /// Returns the previous instance of the given [date] considering the [every]
+ /// base process. If the [date] is valid for the [invalidator], a new
+ /// [DateTime] will be returned.
+ @override
+ DateTime previous(DateTime date, {DateTime? limit}) =>
+ super.previous(date, limit: limit);
+
+ /// Returns `true` if the [date] is valid for the [every] (if it is a
+ /// [DateValidator], like an [EveryDateValidator], for example) and not valid
+ /// for the [invalidator].
+ @override
+ bool valid(DateTime date) {
+ if (every is DateValidator) {
+ final invalid = (every as DateValidator).invalid(date);
+ if (invalid) return false;
+ }
+ return invalidator.invalid(date);
+ }
+
+ /// Returns `true` if the [date] is invalid for the [every] (if it is a
+ /// [DateValidator], like an [EveryDateValidator], for example) and valid for
+ /// the [invalidator].
+ ///
+ /// This is the opposite of [valid].
+ /// Implementations that return true for invalid should also return false for
+ /// valid.
+ ///
+ /// Usually, this will be implemented as `!valid(date)` by the [Every] classes
+ /// that implement [DateValidatorMixin]. However, if there is a simpler way to
+ /// check for invalid dates, it can be implemented here.
+ @override
+ bool invalid(DateTime date) {
+ if (every is DateValidator) {
+ final invalid = (every as DateValidator).invalid(date);
+ if (invalid) return true;
+ }
+ return invalidator.valid(date);
+ }
+
+ @override
+ DateTime processDate(
+ DateTime date,
+ DateDirection direction, {
+ DateTime? limit,
+ }) {
+ if ((limit != null) &&
+ (direction.isPrevious ? date.isBefore(limit) : date.isAfter(limit))) {
+ throw DateTimeLimitReachedException(date: date, limit: limit);
+ }
+ if (invalidator.invalid(date)) return date;
+ if (!direction.isPrevious) return next(date, limit: limit);
+ return previous(date, limit: limit);
+ }
+
+ @override
+ // ignore: hash_and_equals, already implemented by EquatableMixin
+ bool operator ==(Object other) {
+ return (super == other) ||
+ ((other is EverySkipInvalidModifier) &&
+ (other.every == every) &&
+ (other.invalidator == invalidator));
+ }
+
+ @override
+ List