diff --git a/.github/ISSUE_TEMPLATE/clock.md b/.github/ISSUE_TEMPLATE/clock.md new file mode 100644 index 000000000..1ed8f73b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/clock.md @@ -0,0 +1,5 @@ +--- +name: "package:clock" +about: "Create a bug or file a feature request against package:clock." +labels: "package:clock" +--- \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index c3d5de0b9..a6939c7f8 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -12,6 +12,10 @@ - changed-files: - any-glob-to-any-file: 'pkgs/cli_config/**' +'package:clock': + - changed-files: + - any-glob-to-any-file: 'pkgs/clock/**' + 'package:coverage': - changed-files: - any-glob-to-any-file: 'pkgs/coverage/**' diff --git a/.github/workflows/clock.yaml b/.github/workflows/clock.yaml new file mode 100644 index 000000000..1ad4ade04 --- /dev/null +++ b/.github/workflows/clock.yaml @@ -0,0 +1,75 @@ +name: package:clock + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ main ] + paths: + - '.github/workflows/clock.yml' + - 'pkgs/clock/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/clock.yml' + - 'pkgs/clock/**' + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + + +defaults: + run: + working-directory: pkgs/clock/ + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [3.4, dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm + if: always() && steps.install.outcome == 'success' + - name: Run Chrome tests + run: dart test --platform chrome + if: always() && steps.install.outcome == 'success' diff --git a/README.md b/README.md index ac4edf204..f7972895b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ don't naturally belong to other topic monorepos (like | --- | --- | --- | | [boolean_selector](pkgs/boolean_selector/) | A flexible syntax for boolean expressions, based on a simplified version of Dart's expression syntax. | [![pub package](https://img.shields.io/pub/v/boolean_selector.svg)](https://pub.dev/packages/boolean_selector) | | [cli_config](pkgs/cli_config/) | A library to take config values from configuration files, CLI arguments, and environment variables. | [![pub package](https://img.shields.io/pub/v/cli_config.svg)](https://pub.dev/packages/cli_config) | +| [clock](pkgs/clock/) | A fakeable wrapper for dart:core clock APIs. | [![pub package](https://img.shields.io/pub/v/clock.svg)](https://pub.dev/packages/clock) | | [coverage](pkgs/coverage/) | Coverage data manipulation and formatting. | [![pub package](https://img.shields.io/pub/v/coverage.svg)](https://pub.dev/packages/coverage) | | [extension_discovery](pkgs/extension_discovery/) | A convention and utilities for package extension discovery. | [![pub package](https://img.shields.io/pub/v/extension_discovery.svg)](https://pub.dev/packages/extension_discovery) | | [file](pkgs/file/) | A pluggable, mockable file system abstraction for Dart. | [![pub package](https://img.shields.io/pub/v/file.svg)](https://pub.dev/packages/file) | diff --git a/pkgs/clock/.gitignore b/pkgs/clock/.gitignore new file mode 100644 index 000000000..63fe85d83 --- /dev/null +++ b/pkgs/clock/.gitignore @@ -0,0 +1,5 @@ +.packages +.pub/ +.dart_tool/ +build/ +pubspec.lock diff --git a/pkgs/clock/AUTHORS b/pkgs/clock/AUTHORS new file mode 100644 index 000000000..e8063a8cd --- /dev/null +++ b/pkgs/clock/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/pkgs/clock/CHANGELOG.md b/pkgs/clock/CHANGELOG.md new file mode 100644 index 000000000..f372adaa1 --- /dev/null +++ b/pkgs/clock/CHANGELOG.md @@ -0,0 +1,52 @@ +## 1.1.2 + +* Require Dart 3.4 +* Move to `dart-lang/tools` monorepo. + +## 1.1.1 + +* Switch to using `package:lints`. +* Populate the pubspec `repository` field. + +## 1.1.0 + +* Update SDK constraints to `>=2.12.0 <3.0.0`. +* Update to null safety. + +## 1.0.1 + +* Update to lowercase Dart core library constants. + +## 1.0.0 + +This release contains the `Clock` class that was defined in [`quiver`][]. It's +backwards-compatible with the `quiver` version, and *mostly* +backwards-compatible with the old version of the `clock` package. + +[`quiver`]: https://pub.dartlang.org/packages/quiver + +### New Features + +* A top-level `clock` field has been added that provides a default `Clock` + implementation. It can be controlled by the `withClock()` function. It should + generally be used in preference to manual dependency-injection, since it will + work with the [`fake_async`][] package. + +* A `Clock.stopwatch()` method has been added that creates a `Stopwatch` that + uses the clock as its source of time. + +[`fake_async`]: https://pub.dartlang.org/packages/fake_async + +### Changes Relative to `clock` 0.1 + +* The top-level `new` getter and `getStopwatch()` methods are deprecated. + `clock.new()` and `clock.stopwatch()` should be used instead. + +* `Clock.getStopwatch()` is deprecated. `Clock.stopwatch()` should be used instead. + +* The `isFinal` argument to `withClock()` is deprecated. + +* `new Clock()` now takes an optional positional argument that returns the + current time as a `DateTime` instead of its old arguments. + +* `Clock.now()` is now a method rather than a getter. diff --git a/pkgs/clock/LICENSE b/pkgs/clock/LICENSE new file mode 100644 index 000000000..7a4a3ea24 --- /dev/null +++ b/pkgs/clock/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/pkgs/clock/README.md b/pkgs/clock/README.md new file mode 100644 index 000000000..dccdc0755 --- /dev/null +++ b/pkgs/clock/README.md @@ -0,0 +1,52 @@ +[![Build Status](https://github.com/dart-lang/tools/actions/workflows/clock.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/clock.yaml) +[![pub package](https://img.shields.io/pub/v/clock.svg)](https://pub.dev/packages/clock) +[![package publisher](https://img.shields.io/pub/publisher/clock.svg)](https://pub.dev/packages/clock/publisher) + +This package provides a [`Clock`][] class which encapsulates the notion of the +"current time" and provides easy access to points relative to the current time. +Different `Clock`s can have a different notion of the current time, and the +default top-level [`clock`][]'s notion can be swapped out to reliably test +timing-dependent code. + +[`Clock`]: https://pub.dev/documentation/clock/latest/clock/Clock-class.html +[`clock`]: https://pub.dev/documentation/clock/latest/clock/clock.html + +For example, you can use `clock` in your libraries like this: + +```dart +// run_with_timing.dart +import 'package:clock/clock.dart'; + +/// Runs [callback] and prints how long it took. +T runWithTiming(T Function() callback) { + var stopwatch = clock.stopwatch()..start(); + var result = callback(); + print('It took ${stopwatch.elapsed}!'); + return result; +} +``` + +...and then test your code using the [`fake_async`][] package, which +automatically overrides the current clock: + +[`fake_async`]: https://pub.dartlang.org/packages/fake_async + +```dart +// run_with_timing_test.dart +import 'run_with_timing.dart'; + +import 'package:fake_async/fake_async.dart'; +import 'package:test/test.dart'; + +void main() { + test('runWithTiming() prints the elapsed time', () { + FakeAsync().run((async) { + expect(() { + runWithTiming(() { + async.elapse(Duration(seconds: 10)); + }); + }, prints('It took 0:00:10.000000!')); + }); + }); +} +``` diff --git a/pkgs/clock/analysis_options.yaml b/pkgs/clock/analysis_options.yaml new file mode 100644 index 000000000..9ee7c2b6a --- /dev/null +++ b/pkgs/clock/analysis_options.yaml @@ -0,0 +1,14 @@ +# https://dart.dev/guides/language/analysis-options +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - use_super_parameters diff --git a/pkgs/clock/lib/clock.dart b/pkgs/clock/lib/clock.dart new file mode 100644 index 000000000..755789e77 --- /dev/null +++ b/pkgs/clock/lib/clock.dart @@ -0,0 +1,34 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'src/default.dart'; + +export 'src/clock.dart'; +export 'src/default.dart'; + +/// Returns current time. +@Deprecated('Pass around an instance of Clock instead.') +typedef TimeFunction = DateTime Function(); + +/// Returns the current system time. +@Deprecated('Use new DateTime.now() instead.') +DateTime systemTime() => DateTime.now(); + +/// Returns the current time as reported by [clock]. +@Deprecated('Use clock.now() instead.') +DateTime get now => clock.now(); + +/// Returns a stopwatch that uses the current time as reported by [clock]. +@Deprecated('Use clock.stopwatch() instead.') +Stopwatch getStopwatch() => clock.stopwatch(); diff --git a/pkgs/clock/lib/src/clock.dart b/pkgs/clock/lib/src/clock.dart new file mode 100644 index 000000000..f6f47deb1 --- /dev/null +++ b/pkgs/clock/lib/src/clock.dart @@ -0,0 +1,182 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../clock.dart'; +import 'stopwatch.dart'; +import 'utils.dart'; + +/// A provider for the "current time" and points relative to the current time. +/// +/// This class is designed with testability in mind. The current point in time +/// (or [now()]) is defined by a function that returns a [DateTime]. By +/// supplying your own time function or using [Clock.fixed], you can control +/// exactly what time a [Clock] returns and base your test expectations on that. +/// +/// Most users should use the top-level [clock] field, which provides access to +/// a default implementation of [Clock] which can be overridden using +/// [withClock]. +class Clock { + /// The function that's called to determine this clock's notion of the current + /// time. + final DateTime Function() _time; + + /// Creates a clock based on the given [currentTime], or on the system clock + /// by default. + // ignore: deprecated_member_use_from_same_package + const Clock([DateTime Function() currentTime = systemTime]) + : _time = currentTime; + + /// Creates [Clock] that always considers the current time to be [time]. + Clock.fixed(DateTime time) : _time = (() => time); + + /// Returns current time. + DateTime now() => _time(); + + /// Returns the point in time [Duration] amount of time ago. + DateTime agoBy(Duration duration) => now().subtract(duration); + + /// Returns the point in time [Duration] amount of time from now. + DateTime fromNowBy(Duration duration) => now().add(duration); + + /// Returns the point in time that's given amount of time ago. + /// + /// The amount of time is the sum of the individual parts. + DateTime ago( + {int days = 0, + int hours = 0, + int minutes = 0, + int seconds = 0, + int milliseconds = 0, + int microseconds = 0}) => + agoBy(Duration( + days: days, + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + microseconds: microseconds)); + + /// Returns the point in time that's given amount of time from now. + /// + /// The amount of time is the sum of the individual parts. + DateTime fromNow( + {int days = 0, + int hours = 0, + int minutes = 0, + int seconds = 0, + int milliseconds = 0, + int microseconds = 0}) => + fromNowBy(Duration( + days: days, + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + microseconds: microseconds)); + + /// Return the point in time [microseconds] ago. + DateTime microsAgo(int microseconds) => ago(microseconds: microseconds); + + /// Return the point in time [microseconds] from now. + DateTime microsFromNow(int microseconds) => + fromNow(microseconds: microseconds); + + /// Return the point in time [milliseconds] ago. + DateTime millisAgo(int milliseconds) => ago(milliseconds: milliseconds); + + /// Return the point in time [milliseconds] from now. + DateTime millisFromNow(int milliseconds) => + fromNow(milliseconds: milliseconds); + + /// Return the point in time [seconds] ago. + DateTime secondsAgo(int seconds) => ago(seconds: seconds); + + /// Return the point in time [seconds] from now. + DateTime secondsFromNow(int seconds) => fromNow(seconds: seconds); + + /// Return the point in time [minutes] ago. + DateTime minutesAgo(int minutes) => ago(minutes: minutes); + + /// Return the point in time [minutes] from now. + DateTime minutesFromNow(int minutes) => fromNow(minutes: minutes); + + /// Return the point in time [hours] ago. + DateTime hoursAgo(int hours) => ago(hours: hours); + + /// Return the point in time [hours] from now. + DateTime hoursFromNow(int hours) => fromNow(hours: hours); + + /// Return the point in time [days] ago. + DateTime daysAgo(int days) => ago(days: days); + + /// Return the point in time [days] from now. + DateTime daysFromNow(int days) => fromNow(days: days); + + /// Return the point in time [weeks] ago. + DateTime weeksAgo(int weeks) => ago(days: 7 * weeks); + + /// Return the point in time [weeks] from now. + DateTime weeksFromNow(int weeks) => fromNow(days: 7 * weeks); + + /// Return the point in time [months] ago on the same date. + /// + /// If the current day of the month isn't valid in the new month, the nearest + /// valid day in the new month will be used. + DateTime monthsAgo(int months) { + var time = now(); + var month = (time.month - months - 1) % 12 + 1; + var year = time.year - (months + 12 - time.month) ~/ 12; + var day = clampDayOfMonth(year: year, month: month, day: time.day); + return DateTime(year, month, day, time.hour, time.minute, time.second, + time.millisecond); + } + + /// Return the point in time [months] from now on the same date. + /// + /// If the current day of the month isn't valid in the new month, the nearest + /// valid day in the new month will be used. + DateTime monthsFromNow(int months) { + var time = now(); + var month = (time.month + months - 1) % 12 + 1; + var year = time.year + (months + time.month - 1) ~/ 12; + var day = clampDayOfMonth(year: year, month: month, day: time.day); + return DateTime(year, month, day, time.hour, time.minute, time.second, + time.millisecond); + } + + /// Return the point in time [years] ago on the same date. + /// + /// If the current day of the month isn't valid in the new year, the nearest + /// valid day in the original month will be used. + DateTime yearsAgo(int years) { + var time = now(); + var year = time.year - years; + var day = clampDayOfMonth(year: year, month: time.month, day: time.day); + return DateTime(year, time.month, day, time.hour, time.minute, time.second, + time.millisecond); + } + + /// Return the point in time [years] from now on the same date. + /// + /// If the current day of the month isn't valid in the new year, the nearest + /// valid day in the original month will be used. + DateTime yearsFromNow(int years) => yearsAgo(-years); + + /// Returns a new stopwatch that uses the current time as reported by `this`. + Stopwatch stopwatch() => ClockStopwatch(this); + + /// Returns a new stopwatch that uses the current time as reported by `this`. + @Deprecated('Use stopwatch() instead.') + Stopwatch getStopwatch() => stopwatch(); +} diff --git a/pkgs/clock/lib/src/default.dart b/pkgs/clock/lib/src/default.dart new file mode 100644 index 000000000..2a46b9f8b --- /dev/null +++ b/pkgs/clock/lib/src/default.dart @@ -0,0 +1,54 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'clock.dart'; + +/// The key for the [Zone] value that controls the current implementation of +/// [clock]. +final _clockKey = Object(); + +/// The key for the [Zone] value that controls whether nested zones can override +/// [clock]. +final _isFinalKey = Object(); + +/// The default implementation of [clock] for the current [Zone]. +/// +/// This defaults to the system clock. It can be set within a zone using +/// [withClock]. +Clock get clock => Zone.current[_clockKey] as Clock? ?? const Clock(); + +/// Runs [callback] with the given value for the top-level [clock] field. +/// +/// This is [Zone]-scoped, so asynchronous callbacks spawned within [callback] +/// will also use the new value for [clock]. +/// +// ignore: deprecated_member_use_from_same_package +/// If [isFinal] is `true`, calls to [withClock] within [callback] will throw a +/// [StateError]. However, this parameter is deprecated and should be avoided. +T withClock( + Clock clock, + T Function() callback, { + @Deprecated('This parameter is deprecated and should be avoided') + bool isFinal = false, +}) { + if ((Zone.current[_isFinalKey] ?? false) == true) { + throw StateError( + 'Cannot call withClock() within a call to withClock(isFinal = true).'); + } + + return runZoned(callback, + zoneValues: {_clockKey: clock, _isFinalKey: isFinal}); +} diff --git a/pkgs/clock/lib/src/stopwatch.dart b/pkgs/clock/lib/src/stopwatch.dart new file mode 100644 index 000000000..93fe1abc4 --- /dev/null +++ b/pkgs/clock/lib/src/stopwatch.dart @@ -0,0 +1,73 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'clock.dart'; + +/// The system's timer frequency in Hz. +/// +/// We can't really know how frequently the clock is updated, and that may not +/// even make sense for some implementations, so we just pretend we follow the +/// system's frequency. +final _frequency = Stopwatch().frequency; + +/// A stopwatch that gets its notion of the current time from a [Clock]. +class ClockStopwatch implements Stopwatch { + /// The provider for this stopwatch's notion of the current time. + final Clock _clock; + + /// The number of elapsed microseconds that have been recorded from previous + /// runs of this stopwatch. + /// + /// This doesn't include the time between [_start] and the current time. + var _elapsed = 0; + + /// The point at which [start] was called most recently, or `null` if this + /// isn't active. + DateTime? _start; + + ClockStopwatch(this._clock); + + @override + int get frequency => _frequency; + @override + int get elapsedTicks => (elapsedMicroseconds * frequency) ~/ 1000000; + @override + Duration get elapsed => Duration(microseconds: elapsedMicroseconds); + @override + int get elapsedMilliseconds => elapsedMicroseconds ~/ 1000; + @override + bool get isRunning => _start != null; + + @override + int get elapsedMicroseconds => + _elapsed + + (_start == null ? 0 : _clock.now().difference(_start!).inMicroseconds); + + @override + void start() { + _start ??= _clock.now(); + } + + @override + void stop() { + _elapsed = elapsedMicroseconds; + _start = null; + } + + @override + void reset() { + _elapsed = 0; + if (_start != null) _start = _clock.now(); + } +} diff --git a/pkgs/clock/lib/src/utils.dart b/pkgs/clock/lib/src/utils.dart new file mode 100644 index 000000000..4301b099d --- /dev/null +++ b/pkgs/clock/lib/src/utils.dart @@ -0,0 +1,59 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This code is copied from quiver. We don't take on an explicit dependency +// because quiver is very large and the amount of code we use from it is very +// small. + +/// The number of days in each month. +/// +/// This array uses 1-based month numbers, i.e. January is the 1-st element in +/// the array, not the 0-th. +const _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +/// Returns the number of days in the specified month. +/// +/// This function assumes the use of the Gregorian calendar or the proleptic +/// Gregorian calendar. +int daysInMonth(int year, int month) => + (month == DateTime.february && isLeapYear(year)) ? 29 : _daysInMonth[month]; + +/// Returns true if [year] is a leap year. +/// +/// This implements the Gregorian calendar leap year rules wherein a year is +/// considered to be a leap year if it is divisible by 4, excepting years +/// divisible by 100, but including years divisible by 400. +/// +/// This function assumes the use of the Gregorian calendar or the proleptic +/// Gregorian calendar. +bool isLeapYear(int year) => + year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + +/// Takes a `date` that may be outside the allowed range of dates for a given +/// [month] in a given [year] and returns the closest date that is within the +/// allowed range. +/// +/// For example: +/// +/// February 31, 2013 => February 28, 2013 +/// +/// When jumping from month to month or from leap year to common year we may +/// end up in a month that has fewer days than the month we are jumping from. +/// In that case it is impossible to preserve the exact date. So we "clamp" the +/// date value to fit within the month. For example, jumping from March 31 one +/// month back takes us to February 28 (or 29 during a leap year), as February +/// doesn't have 31-st date. +int clampDayOfMonth( + {required int year, required int month, required int day}) => + day.clamp(1, daysInMonth(year, month)); diff --git a/pkgs/clock/pubspec.yaml b/pkgs/clock/pubspec.yaml new file mode 100644 index 000000000..2487fe778 --- /dev/null +++ b/pkgs/clock/pubspec.yaml @@ -0,0 +1,11 @@ +name: clock +version: 1.1.2 +description: A fakeable wrapper for dart:core clock APIs. +repository: https://github.com/dart-lang/tools/tree/main/pkgs/clock + +environment: + sdk: ^3.4.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 + test: ^1.16.6 diff --git a/pkgs/clock/test/clock_test.dart b/pkgs/clock/test/clock_test.dart new file mode 100644 index 000000000..c457153df --- /dev/null +++ b/pkgs/clock/test/clock_test.dart @@ -0,0 +1,210 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the 'License'); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:clock/clock.dart'; + +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + late Clock clock; + setUp(() { + clock = Clock.fixed(date(2013)); + }); + + test('should return a non-null value from system clock', () { + expect(const Clock().now(), isNotNull); + }); + + // This test may be flaky on certain systems. I ran it over 10 million + // cycles on my machine without any failures, but that's no guarantee. + test('should be close enough to system clock', () { + // At 10ms the test doesn't seem to be flaky. + var epsilon = 10; + expect(DateTime.now().difference(const Clock().now()).inMilliseconds.abs(), + lessThan(epsilon)); + expect(DateTime.now().difference(const Clock().now()).inMilliseconds.abs(), + lessThan(epsilon)); + }); + + test('should return time provided by a custom function', () { + var time = date(2013); + var fixedClock = Clock(() => time); + expect(fixedClock.now(), date(2013)); + + time = date(2014); + expect(fixedClock.now(), date(2014)); + }); + + test('should return fixed time', () { + expect(Clock.fixed(date(2013)).now(), date(2013)); + }); + + test('should return time Duration ago', () { + expect(clock.agoBy(const Duration(days: 366)), date(2012)); + }); + + test('should return time Duration from now', () { + expect(clock.fromNowBy(const Duration(days: 365)), date(2014)); + }); + + test('should return time parts ago', () { + expect( + clock.ago( + days: 1, + hours: 1, + minutes: 1, + seconds: 1, + milliseconds: 1, + microseconds: 1000), + DateTime(2012, 12, 30, 22, 58, 58, 998)); + }); + + test('should return time parts from now', () { + expect( + clock.fromNow( + days: 1, + hours: 1, + minutes: 1, + seconds: 1, + milliseconds: 1, + microseconds: 1000), + DateTime(2013, 1, 2, 1, 1, 1, 2)); + }); + + test('should return time micros ago', () { + expect(clock.microsAgo(1000), DateTime(2012, 12, 31, 23, 59, 59, 999)); + }); + + test('should return time micros from now', () { + expect(clock.microsFromNow(1000), DateTime(2013, 1, 1, 0, 0, 0, 1)); + }); + + test('should return time millis ago', () { + expect(clock.millisAgo(1000), DateTime(2012, 12, 31, 23, 59, 59)); + }); + + test('should return time millis from now', () { + expect(clock.millisFromNow(3), DateTime(2013, 1, 1, 0, 0, 0, 3)); + }); + + test('should return time seconds ago', () { + expect(clock.secondsAgo(10), DateTime(2012, 12, 31, 23, 59, 50)); + }); + + test('should return time seconds from now', () { + expect(clock.secondsFromNow(3), DateTime(2013, 1, 1, 0, 0, 3)); + }); + + test('should return time minutes ago', () { + expect(clock.minutesAgo(10), DateTime(2012, 12, 31, 23, 50)); + }); + + test('should return time minutes from now', () { + expect(clock.minutesFromNow(3), DateTime(2013, 1, 1, 0, 3)); + }); + + test('should return time hours ago', () { + expect(clock.hoursAgo(10), DateTime(2012, 12, 31, 14)); + }); + + test('should return time hours from now', () { + expect(clock.hoursFromNow(3), DateTime(2013, 1, 1, 3)); + }); + + test('should return time days ago', () { + expect(clock.daysAgo(10), date(2012, 12, 22)); + }); + + test('should return time days from now', () { + expect(clock.daysFromNow(3), date(2013, 1, 4)); + }); + + test('should return time months ago on the same date', () { + expect(clock.monthsAgo(1), date(2012, 12, 1)); + expect(clock.monthsAgo(2), date(2012, 11, 1)); + expect(clock.monthsAgo(3), date(2012, 10, 1)); + expect(clock.monthsAgo(4), date(2012, 9, 1)); + }); + + test('should return time months from now on the same date', () { + expect(clock.monthsFromNow(1), date(2013, 2, 1)); + expect(clock.monthsFromNow(2), date(2013, 3, 1)); + expect(clock.monthsFromNow(3), date(2013, 4, 1)); + expect(clock.monthsFromNow(4), date(2013, 5, 1)); + }); + + test('should go from 2013-05-31 to 2012-11-30', () { + expect(fixed(2013, 5, 31).monthsAgo(6), date(2012, 11, 30)); + }); + + test('should go from 2013-03-31 to 2013-02-28 (common year)', () { + expect(fixed(2013, 3, 31).monthsAgo(1), date(2013, 2, 28)); + }); + + test('should go from 2013-05-31 to 2013-02-28 (common year)', () { + expect(fixed(2013, 5, 31).monthsAgo(3), date(2013, 2, 28)); + }); + + test('should go from 2004-03-31 to 2004-02-29 (leap year)', () { + expect(fixed(2004, 3, 31).monthsAgo(1), date(2004, 2, 29)); + }); + + test('should go from 2013-03-31 to 2013-06-30', () { + expect(fixed(2013, 3, 31).monthsFromNow(3), date(2013, 6, 30)); + }); + + test('should go from 2003-12-31 to 2004-02-29 (common to leap)', () { + expect(fixed(2003, 12, 31).monthsFromNow(2), date(2004, 2, 29)); + }); + + test('should go from 2004-02-29 to 2003-02-28 by year', () { + expect(fixed(2004, 2, 29).yearsAgo(1), date(2003, 2, 28)); + }); + + test('should go from 2004-02-29 to 2003-02-28 by month', () { + expect(fixed(2004, 2, 29).monthsAgo(12), date(2003, 2, 28)); + }); + + test('should go from 2004-02-29 to 2005-02-28 by year', () { + expect(fixed(2004, 2, 29).yearsFromNow(1), date(2005, 2, 28)); + }); + + test('should go from 2004-02-29 to 2005-02-28 by month', () { + expect(fixed(2004, 2, 29).monthsFromNow(12), date(2005, 2, 28)); + }); + + test('should return time years ago on the same date', () { + expect(clock.yearsAgo(1), date(2012, 1, 1)); // leap year + expect(clock.yearsAgo(2), date(2011, 1, 1)); + expect(clock.yearsAgo(3), date(2010, 1, 1)); + expect(clock.yearsAgo(4), date(2009, 1, 1)); + expect(clock.yearsAgo(5), date(2008, 1, 1)); // leap year + expect(clock.yearsAgo(6), date(2007, 1, 1)); + expect(clock.yearsAgo(30), date(1983, 1, 1)); + expect(clock.yearsAgo(2013), date(0, 1, 1)); + }); + + test('should return time years from now on the same date', () { + expect(clock.yearsFromNow(1), date(2014, 1, 1)); + expect(clock.yearsFromNow(2), date(2015, 1, 1)); + expect(clock.yearsFromNow(3), date(2016, 1, 1)); + expect(clock.yearsFromNow(4), date(2017, 1, 1)); + expect(clock.yearsFromNow(5), date(2018, 1, 1)); + expect(clock.yearsFromNow(6), date(2019, 1, 1)); + expect(clock.yearsFromNow(30), date(2043, 1, 1)); + expect(clock.yearsFromNow(1000), date(3013, 1, 1)); + }); +} diff --git a/pkgs/clock/test/default_test.dart b/pkgs/clock/test/default_test.dart new file mode 100644 index 000000000..80acd15bb --- /dev/null +++ b/pkgs/clock/test/default_test.dart @@ -0,0 +1,84 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the 'License'); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:clock/clock.dart'; + +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('the default clock returns the system time', () { + expect(DateTime.now().difference(clock.now()).inMilliseconds.abs(), + lessThan(100)); + }); + + group('withClock()', () { + group('overrides the clock', () { + test('synchronously', () { + var time = date(1990, 11, 8); + withClock(Clock(() => time), () { + expect(clock.now(), equals(time)); + time = date(2016, 6, 26); + expect(clock.now(), equals(time)); + }); + }); + + test('asynchronously', () { + var time = date(1990, 11, 8); + withClock(Clock.fixed(time), () { + expect(Future(() async { + expect(clock.now(), equals(time)); + }), completes); + }); + }); + + test('within another withClock() call', () { + var outerTime = date(1990, 11, 8); + withClock(Clock.fixed(outerTime), () { + expect(clock.now(), equals(outerTime)); + + var innerTime = date(2016, 11, 8); + withClock(Clock.fixed(innerTime), () { + expect(clock.now(), equals(innerTime)); + expect(Future(() async { + expect(clock.now(), equals(innerTime)); + }), completes); + }); + + expect(clock.now(), equals(outerTime)); + }); + }); + }); + + test("with isFinal: true doesn't allow nested calls", () { + var outerTime = date(1990, 11, 8); + withClock(Clock.fixed(outerTime), () { + expect(clock.now(), equals(outerTime)); + + expect(() => withClock(fixed(2016, 11, 8), neverCalledVoid), + throwsStateError); + + expect(clock.now(), equals(outerTime)); + // ignore: deprecated_member_use_from_same_package + }, isFinal: true); + }); + }); +} + +/// A wrapper for [neverCalled] that works around sdk#33015. +void Function() get neverCalledVoid { + var function = neverCalled; + return () => function(); +} diff --git a/pkgs/clock/test/stopwatch_test.dart b/pkgs/clock/test/stopwatch_test.dart new file mode 100644 index 000000000..e96c0f7d0 --- /dev/null +++ b/pkgs/clock/test/stopwatch_test.dart @@ -0,0 +1,171 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the 'License'); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:clock/clock.dart'; + +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('returns the system frequency', () { + expect(fixed(1990, 11, 8).stopwatch().frequency, + equals(Stopwatch().frequency)); + }); + + group('before it starts', () { + late Stopwatch stopwatch; + setUp(() { + stopwatch = clock.stopwatch(); + }); + + test('is not running', () => expect(stopwatch.isRunning, isFalse)); + + test('stop() does nothing', () { + stopwatch.stop(); + expect(stopwatch.isRunning, isFalse); + expect(stopwatch.elapsed, equals(Duration.zero)); + }); + + group('reports no elapsed', () { + test('duration', () => expect(stopwatch.elapsed, equals(Duration.zero))); + test('ticks', () => expect(stopwatch.elapsedTicks, isZero)); + test('microseconds', () => expect(stopwatch.elapsedMicroseconds, isZero)); + test('milliseconds', () => expect(stopwatch.elapsedMilliseconds, isZero)); + }); + }); + + group('when 12345μs have elapsed', () { + late DateTime time; + late Clock clock; + late Stopwatch stopwatch; + setUp(() { + time = date(1990, 11, 8); + clock = Clock(() => time); + stopwatch = clock.stopwatch()..start(); + time = clock.microsFromNow(12345); + }); + + group('and the stopwatch is active', () { + test('is running', () { + expect(stopwatch.isRunning, isTrue); + }); + + test('reports more elapsed time', () { + time = clock.microsFromNow(54321); + expect(stopwatch.elapsedMicroseconds, equals(66666)); + }); + + test('start does nothing', () { + stopwatch.start(); + expect(stopwatch.isRunning, isTrue); + expect(stopwatch.elapsedMicroseconds, equals(12345)); + }); + + group('reset()', () { + setUp(() { + stopwatch.reset(); + }); + + test('sets the elapsed time to zero', () { + expect(stopwatch.elapsed, equals(Duration.zero)); + }); + + test('reports more elapsed time', () { + time = clock.microsFromNow(54321); + expect(stopwatch.elapsedMicroseconds, equals(54321)); + }); + }); + + group('reports elapsed', () { + test('duration', () { + expect( + stopwatch.elapsed, equals(const Duration(microseconds: 12345))); + }); + + test('ticks', () { + expect(stopwatch.elapsedTicks, + equals((Stopwatch().frequency * 12345) ~/ 1000000)); + }); + + test('microseconds', () { + expect(stopwatch.elapsedMicroseconds, equals(12345)); + }); + + test('milliseconds', () { + expect(stopwatch.elapsedMilliseconds, equals(12)); + }); + }); + }); + + group('and the stopwatch is inactive, reports that as', () { + setUp(() { + stopwatch.stop(); + }); + + test('is not running', () { + expect(stopwatch.isRunning, isFalse); + }); + + test("doesn't report more elapsed time", () { + time = clock.microsFromNow(54321); + expect(stopwatch.elapsedMicroseconds, equals(12345)); + }); + + test('start starts reporting more elapsed time', () { + stopwatch.start(); + expect(stopwatch.isRunning, isTrue); + time = clock.microsFromNow(54321); + expect(stopwatch.elapsedMicroseconds, equals(66666)); + }); + + group('reset()', () { + setUp(() { + stopwatch.reset(); + }); + + test('sets the elapsed time to zero', () { + expect(stopwatch.elapsed, equals(Duration.zero)); + }); + + test("doesn't report more elapsed time", () { + time = clock.microsFromNow(54321); + expect(stopwatch.elapsed, equals(Duration.zero)); + }); + }); + + group('reports elapsed', () { + test('duration', () { + expect( + stopwatch.elapsed, equals(const Duration(microseconds: 12345))); + }); + + test('ticks', () { + expect(stopwatch.elapsedTicks, + equals((Stopwatch().frequency * 12345) ~/ 1000000)); + }); + + test('microseconds', () { + expect(stopwatch.elapsedMicroseconds, equals(12345)); + }); + + test('milliseconds', () { + expect(stopwatch.elapsedMilliseconds, equals(12)); + }); + }); + }); + }, onPlatform: { + 'js': const Skip('Web does not have enough precision'), + }); +} diff --git a/pkgs/clock/test/utils.dart b/pkgs/clock/test/utils.dart new file mode 100644 index 000000000..ea4b1df68 --- /dev/null +++ b/pkgs/clock/test/utils.dart @@ -0,0 +1,25 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:clock/clock.dart'; + +/// A utility function for tersely constructing a [DateTime] with no time +/// component. +DateTime date(int year, [int? month, int? day]) => + DateTime(year, month ?? 1, day ?? 1); + +/// Returns a clock that always returns a date with the given [year], [month], +/// and [day]. +Clock fixed(int year, [int? month, int? day]) => + Clock.fixed(date(year, month, day));