diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json new file mode 100644 index 0000000..eeaf2e4 --- /dev/null +++ b/.dart_tool/package_config.json @@ -0,0 +1,308 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "_fe_analyzer_shared", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/_fe_analyzer_shared-12.0.0", + "packageUri": "lib/", + "languageVersion": "2.2" + }, + { + "name": "analyzer", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/analyzer-0.40.6", + "packageUri": "lib/", + "languageVersion": "2.7" + }, + { + "name": "args", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/args-1.6.0", + "packageUri": "lib/", + "languageVersion": "2.3" + }, + { + "name": "async", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/async-2.4.2", + "packageUri": "lib/", + "languageVersion": "2.2" + }, + { + "name": "boolean_selector", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/boolean_selector-2.0.0", + "packageUri": "lib/", + "languageVersion": "2.4" + }, + { + "name": "charcode", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/charcode-1.1.3", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "cli_util", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/cli_util-0.2.0", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "collection", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/collection-1.14.13", + "packageUri": "lib/", + "languageVersion": "2.3" + }, + { + "name": "convert", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/convert-2.1.1", + "packageUri": "lib/", + "languageVersion": "1.17" + }, + { + "name": "coverage", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/coverage-0.14.1", + "packageUri": "lib/", + "languageVersion": "2.7" + }, + { + "name": "crypto", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/crypto-2.1.5", + "packageUri": "lib/", + "languageVersion": "2.3" + }, + { + "name": "glob", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/glob-1.2.0", + "packageUri": "lib/", + "languageVersion": "2.1" + }, + { + "name": "http", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/http-0.12.2", + "packageUri": "lib/", + "languageVersion": "2.4" + }, + { + "name": "http_multi_server", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/http_multi_server-2.2.0", + "packageUri": "lib/", + "languageVersion": "2.1" + }, + { + "name": "http_parser", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/http_parser-3.1.4", + "packageUri": "lib/", + "languageVersion": "2.3" + }, + { + "name": "io", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/io-0.3.4", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "js", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/js-0.6.2", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "logging", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/logging-0.11.4", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "matcher", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/matcher-0.12.9", + "packageUri": "lib/", + "languageVersion": "2.7" + }, + { + "name": "meta", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/meta-1.2.3", + "packageUri": "lib/", + "languageVersion": "1.12" + }, + { + "name": "mime", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/mime-0.9.7", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "node_interop", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/node_interop-1.2.0", + "packageUri": "lib/", + "languageVersion": "2.9" + }, + { + "name": "node_io", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/node_io-1.1.1", + "packageUri": "lib/", + "languageVersion": "2.2" + }, + { + "name": "node_preamble", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/node_preamble-1.4.12", + "packageUri": "lib/", + "languageVersion": "1.24" + }, + { + "name": "package_config", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/package_config-1.9.3", + "packageUri": "lib/", + "languageVersion": "2.7" + }, + { + "name": "path", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/path-1.7.0", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "pedantic", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/pedantic-1.9.2", + "packageUri": "lib/", + "languageVersion": "2.1" + }, + { + "name": "pool", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/pool-1.4.0", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "pub_semver", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/pub_semver-1.4.4", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "shelf", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/shelf-0.7.9", + "packageUri": "lib/", + "languageVersion": "2.1" + }, + { + "name": "shelf_packages_handler", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/shelf_packages_handler-2.0.0", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "shelf_static", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/shelf_static-0.2.8", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "shelf_web_socket", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/shelf_web_socket-0.2.3", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "source_map_stack_trace", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/source_map_stack_trace-2.0.0", + "packageUri": "lib/", + "languageVersion": "2.7" + }, + { + "name": "source_maps", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/source_maps-0.10.9", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "source_span", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/source_span-1.7.0", + "packageUri": "lib/", + "languageVersion": "2.6" + }, + { + "name": "stack_trace", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/stack_trace-1.9.5", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "stream_channel", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/stream_channel-2.0.0", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "string_scanner", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/string_scanner-1.0.5", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "term_glyph", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/term_glyph-1.1.0", + "packageUri": "lib/", + "languageVersion": "1.8" + }, + { + "name": "test", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/test-1.15.4", + "packageUri": "lib/", + "languageVersion": "2.7" + }, + { + "name": "test_api", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/test_api-0.2.18", + "packageUri": "lib/", + "languageVersion": "2.7" + }, + { + "name": "test_core", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/test_core-0.3.11+1", + "packageUri": "lib/", + "languageVersion": "2.7" + }, + { + "name": "typed_data", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/typed_data-1.2.0", + "packageUri": "lib/", + "languageVersion": "2.4" + }, + { + "name": "vm_service", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/vm_service-4.2.0", + "packageUri": "lib/", + "languageVersion": "2.6" + }, + { + "name": "watcher", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/watcher-0.9.7+15", + "packageUri": "lib/", + "languageVersion": "2.2" + }, + { + "name": "web_socket_channel", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/web_socket_channel-1.1.0", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "webkit_inspection_protocol", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/webkit_inspection_protocol-0.7.3", + "packageUri": "lib/", + "languageVersion": "2.0" + }, + { + "name": "yaml", + "rootUri": "file:///C:/Users/Leandro/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/yaml-2.2.1", + "packageUri": "lib/", + "languageVersion": "2.4" + }, + { + "name": "cron", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "2.7" + } + ], + "generated": "2020-11-05T15:09:11.407230Z", + "generator": "pub", + "generatorVersion": "2.10.2" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 095ad4c..ed70d11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.5 + +- Added support for special characters `?` and `L` and a 7th attribute for the `year` +- Factory of cron params into single classes to increase maintenance and readability + ## 0.2.4 - Support for `seconds` and 6-part String format. diff --git a/example/every.dart b/example/every.dart new file mode 100644 index 0000000..39100f1 --- /dev/null +++ b/example/every.dart @@ -0,0 +1,14 @@ +import 'package:cron/cron.dart'; + +void main() async { + final cron = Cron(); + + /// Run the task every 30 seconds only on Saturdays during the year of 2020 + /// you could also use `6L` instead of just `L` and specify a different day of the week + final task = cron.schedule(Schedule.parse('*/30 * * * * * 2020'), () { + print('Runned at ${DateTime.now()}'); + }); + + /// Don't forget to close the task if you don't need it anymore + /// by running `task.cancel()` +} diff --git a/example/everyday.dart b/example/everyday.dart new file mode 100644 index 0000000..81fc427 --- /dev/null +++ b/example/everyday.dart @@ -0,0 +1,14 @@ +import 'package:cron/cron.dart'; + +void main() async { + final cron = Cron(); + + /// Schedule that will tell the cron to run every day from Monday through Friday at 7AM + /// You could also use the syntax `0 0 7 * * 1-5 *` + final task = cron.schedule(Schedule.parse('0 0 7 ? * MON-FRI *'), () { + print('Runned at ${DateTime.now()}'); + }); + + /// Don't forget to close the task if you don't need it anymore + /// by running `task.cancel()` +} diff --git a/example/example.dart b/example/example.dart deleted file mode 100644 index cdcf060..0000000 --- a/example/example.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:cron/cron.dart'; - -Future main() async { - final cron = Cron() - ..schedule(Schedule.parse('*/6 * * * * *'), () { - print(DateTime.now()); - }); - await Future.delayed(Duration(seconds: 20)); - await cron.close(); -} diff --git a/example/lastday.dart b/example/lastday.dart new file mode 100644 index 0000000..c6988b2 --- /dev/null +++ b/example/lastday.dart @@ -0,0 +1,14 @@ +import 'package:cron/cron.dart'; + +void main() async { + final cron = Cron(); + + /// Schedule that will tell the cron to run on Friday of each month at 7AM + /// You could also use the syntax `0 0 7 * * 5L *` + final task = cron.schedule(Schedule.parse('0 0 7 ? * 5L *'), () { + print('Runned at ${DateTime.now()}'); + }); + + /// Don't forget to close the task if you don't need it anymore + /// by running `task.cancel()` +} diff --git a/lib/cron.dart b/lib/cron.dart index aebd9a1..40e5d8f 100644 --- a/lib/cron.dart +++ b/lib/cron.dart @@ -3,7 +3,17 @@ import 'dart:async'; -import 'src/constraint_parser.dart'; +part 'src/cron_impl.dart'; +part 'src/scheduler.dart'; + +part 'src/params/cron_param.dart'; +part 'src/params/days_param.dart'; +part 'src/params/hours_param.dart'; +part 'src/params/minutes_param.dart'; +part 'src/params/months_param.dart'; +part 'src/params/seconds_param.dart'; +part 'src/params/weekday_param.dart'; +part 'src/params/years_param.dart'; /// A task may return a Future to indicate when it is completed. If it wouldn't /// complete before [Cron] calls it again, it will be delayed. @@ -20,195 +30,3 @@ abstract class Cron { /// Closes the cron instance and doesn't accept new tasks anymore. Future close(); } - -/// The cron schedule. -class Schedule { - /// The seconds a Task should be started. - final List seconds; - - /// The minutes a Task should be started. - final List minutes; - - /// The hours a Task should be started. - final List hours; - - /// The days a Task should be started. - final List days; - - /// The months a Task should be started. - final List months; - - /// The weekdays a Task should be started. - final List weekdays; - - factory Schedule({ - /// The seconds a Task should be started. - /// Can be one of `int`, `List` or `String` or `null` (= match all). - dynamic seconds, - - /// The minutes a Task should be started. - /// Can be one of `int`, `List` or `String` or `null` (= match all). - dynamic minutes, - - /// The hours a Task should be started. - /// Can be one of `int`, `List` or `String` or `null` (= match all). - dynamic hours, - - /// The days a Task should be started. - /// Can be one of `int`, `List` or `String` or `null` (= match all). - dynamic days, - - /// The months a Task should be started. - /// Can be one of `int`, `List` or `String` or `null` (= match all). - dynamic months, - - /// The weekdays a Task should be started. - /// Can be one of `int`, `List` or `String` or `null` (= match all). - dynamic weekdays, - }) { - final parsedSeconds = - parseConstraint(seconds)?.where((x) => x >= 0 && x <= 59)?.toList(); - final parsedMinutes = - parseConstraint(minutes)?.where((x) => x >= 0 && x <= 59)?.toList(); - final parsedHours = - parseConstraint(hours)?.where((x) => x >= 0 && x <= 59)?.toList(); - final parsedDays = - parseConstraint(days)?.where((x) => x >= 1 && x <= 31)?.toList(); - final parsedMonths = - parseConstraint(months)?.where((x) => x >= 1 && x <= 12)?.toList(); - final parsedWeekdays = parseConstraint(weekdays) - ?.where((x) => x >= 0 && x <= 7) - ?.map((x) => x == 0 ? 7 : x) - ?.toSet() - ?.toList(); - return Schedule._(parsedSeconds, parsedMinutes, parsedHours, parsedDays, - parsedMonths, parsedWeekdays); - } - - /// Parses the cron-formatted text and creates a schedule out of it. - factory Schedule.parse(String cronFormat) { - final p = cronFormat.split(RegExp('\\s+')); - assert(p.length == 5 || p.length == 6); - final parts = [ - if (p.length == 5) null, - ...p, - ]; - return Schedule( - seconds: parts[0], - minutes: parts[1], - hours: parts[2], - days: parts[3], - months: parts[4], - weekdays: parts[5], - ); - } - - Schedule._(this.seconds, this.minutes, this.hours, this.days, this.months, - this.weekdays); - - bool get _hasSeconds => - seconds != null && - seconds.isNotEmpty && - (seconds.length != 1 || !seconds.contains(0)); -} - -abstract class ScheduledTask { - Schedule get schedule; - Future cancel(); -} - -const int _millisecondsPerSecond = 1000; - -class _Cron implements Cron { - bool _closed = false; - Timer _timer; - final _schedules = <_ScheduledTask>[]; - - @override - ScheduledTask schedule(Schedule schedule, Task task) { - if (_closed) throw Exception('Closed.'); - final st = _ScheduledTask(schedule, task); - _schedules.add(st); - _scheduleNextTick(); - return st; - } - - @override - Future close() async { - _closed = true; - _timer?.cancel(); - _timer = null; - for (final schedule in _schedules) { - await schedule.cancel(); - } - } - - void _scheduleNextTick() { - if (_closed) return; - if (_timer != null || _schedules.isEmpty) return; - final now = DateTime.now(); - final isTickMinute = _schedules.any((task) => !task.schedule._hasSeconds); - final ms = (isTickMinute ? 60 : 1) * _millisecondsPerSecond - - (now.millisecondsSinceEpoch % - ((isTickMinute ? 60 : 1) * _millisecondsPerSecond)); - _timer = Timer(Duration(milliseconds: ms), _tick); - } - - void _tick() { - _timer = null; - final now = DateTime.now(); - for (final schedule in _schedules) { - schedule.tick(now); - } - _scheduleNextTick(); - } -} - -class _ScheduledTask implements ScheduledTask { - @override - final Schedule schedule; - final Task _task; - - bool _closed = false; - Future _running; - bool _overrun = false; - - _ScheduledTask(this.schedule, this._task); - - void tick(DateTime now) { - if (_closed) return; - if (schedule?.seconds?.contains(now.second) == false) return; - if (schedule?.minutes?.contains(now.minute) == false) return; - if (schedule?.hours?.contains(now.hour) == false) return; - if (schedule?.days?.contains(now.day) == false) return; - if (schedule?.months?.contains(now.month) == false) return; - if (schedule?.weekdays?.contains(now.weekday) == false) return; - _run(); - } - - void _run() { - if (_closed) return; - if (_running != null) { - _overrun = true; - return; - } - _running = - Future.microtask(() => _task()).then((_) => null, onError: (_) => null); - _running.whenComplete(() { - _running = null; - if (_overrun) { - _overrun = false; - _run(); - } - }); - } - - @override - Future cancel() async { - _closed = true; - _overrun = false; - if (_running != null) { - await _running; - } - } -} diff --git a/lib/src/constraint_parser.dart b/lib/src/constraint_parser.dart deleted file mode 100644 index 61d7b26..0000000 --- a/lib/src/constraint_parser.dart +++ /dev/null @@ -1,37 +0,0 @@ -List parseConstraint(dynamic constraint) { - if (constraint == null) return null; - if (constraint is int) return [constraint]; - if (constraint is List) return constraint; - if (constraint is String) { - if (constraint == '*' || constraint == '') return null; - final parts = constraint.split(','); - if (parts.length > 1) { - final items = - parts.map(parseConstraint).expand((list) => list).toSet().toList(); - items.sort(); - return items; - } - - final singleValue = int.tryParse(constraint); - if (singleValue != null) return [singleValue]; - - if (constraint.startsWith('*/')) { - final period = int.tryParse(constraint.substring(2)) ?? -1; - if (period > 0) { - return List.generate(120 ~/ period, (i) => i * period); - } - } - - if (constraint.contains('-')) { - final ranges = constraint.split('-'); - if (ranges.length == 2) { - final lower = int.tryParse(ranges.first) ?? -1; - final higher = int.tryParse(ranges.last) ?? -1; - if (lower <= higher) { - return List.generate(higher - lower + 1, (i) => i + lower); - } - } - } - } - throw Exception('Unable to parse: $constraint'); -} diff --git a/lib/src/cron_impl.dart b/lib/src/cron_impl.dart new file mode 100644 index 0000000..2e22435 --- /dev/null +++ b/lib/src/cron_impl.dart @@ -0,0 +1,91 @@ +part of '../cron.dart'; + +const int _millisecondsPerSecond = 1000; + +class _Cron implements Cron { + bool _closed = false; + Timer _timer; + final _schedules = <_ScheduledTask>[]; + + @override + ScheduledTask schedule(Schedule schedule, Task task) { + if (_closed) throw Exception('Closed.'); + final st = _ScheduledTask(schedule, task); + _schedules.add(st); + _scheduleNextTick(); + return st; + } + + @override + Future close() async { + _closed = true; + _timer?.cancel(); + _timer = null; + for (final schedule in _schedules) { + await schedule.cancel(); + } + } + + void _scheduleNextTick() { + if (_closed) return; + if (_timer != null || _schedules.isEmpty) return; + final now = DateTime.now(); + final isTickMinute = _schedules.any((task) => !task.schedule._hasSeconds); + final ms = (isTickMinute ? 60 : 1) * _millisecondsPerSecond - + (now.millisecondsSinceEpoch % + ((isTickMinute ? 60 : 1) * _millisecondsPerSecond)); + _timer = Timer(Duration(milliseconds: ms), _tick); + } + + void _tick() { + _timer = null; + final now = DateTime.now(); + for (final schedule in _schedules) { + schedule.tick(now); + } + _scheduleNextTick(); + } +} + +class _ScheduledTask implements ScheduledTask { + @override + final Schedule schedule; + final Task _task; + + bool _closed = false; + Future _running; + bool _overrun = false; + + _ScheduledTask(this.schedule, this._task); + + void tick(DateTime now) { + if (_closed || schedule.params.any((p) => !p.shouldTick(now))) return; + _run(); + } + + void _run() { + if (_closed) return; + if (_running != null) { + _overrun = true; + return; + } + _running = + Future.microtask(() => _task()).then((_) => null, onError: (_) => null); + _running.whenComplete(() { + _running = null; + if (_overrun) { + _overrun = false; + _run(); + } + }); + } + + @override + Future cancel() async { + _closed = true; + _overrun = false; + if (_running != null) { + await _running; + } + } +} diff --git a/lib/src/params/cron_param.dart b/lib/src/params/cron_param.dart new file mode 100644 index 0000000..8cc1c4e --- /dev/null +++ b/lib/src/params/cron_param.dart @@ -0,0 +1,80 @@ +part of '../../cron.dart'; + +Map _weekdayNameToInt = { + 'SUN': 7, + 'SUNDAY': 7, + 'MON': 1, + 'MONDAY': 1, + 'TUE': 2, + 'TUES': 2, + 'TUESDAY': 2, + 'WED': 3, + 'WEDNESDAY': 3, + 'THU': 4, + 'THURS': 4, + 'THURSDAY': 4, + 'FRI': 5, + 'FRIDAY': 5, + 'SAT': 6, + 'SATURDAY': 6, +}; + +int _getWeekdayIntFromName(String name) { + if (_weekdayNameToInt.containsKey(name)) return _weekdayNameToInt[name]; + return null; +} + +abstract class _CronParam { + bool shouldTick(DateTime now); + + /// REturn true if the given value shall be setted as a `null` value + /// Currently `null`, `* `, `?` and emptyValues will return true; + static bool isNull(dynamic value) => + value == null || value == '*' || value == '' || value == '?'; + + List parseConstraint(dynamic constraint) { + if (isNull(constraint)) return null; + + if (constraint is int) return [constraint]; + if (constraint is List) return constraint; + + if (constraint is String) { + final parts = constraint.split(','); + if (parts.length > 1) { + final items = + parts.map(parseConstraint).expand((list) => list).toSet().toList(); + items.sort(); + return items; + } + + final singleValue = + int.tryParse(constraint) ?? _getWeekdayIntFromName(constraint); + if (singleValue != null) return [singleValue]; + + if (constraint.startsWith('*/')) { + final period = int.tryParse(constraint.substring(2)) ?? -1; + if (period > 0) { + return List.generate(120 ~/ period, (i) => i * period); + } + } + + if (constraint.contains('-')) { + final ranges = constraint.toUpperCase().split('-'); + if (ranges.length == 2) { + final lower = int.tryParse(ranges.first) ?? + _getWeekdayIntFromName(ranges.first) ?? + -1; + + final higher = int.tryParse(ranges.last) ?? + _getWeekdayIntFromName(ranges.last) ?? + -1; + + if (lower <= higher) { + return List.generate(higher - lower + 1, (i) => i + lower); + } + } + } + } + throw Exception('Unable to parse: $constraint'); + } +} diff --git a/lib/src/params/days_param.dart b/lib/src/params/days_param.dart new file mode 100644 index 0000000..994a03f --- /dev/null +++ b/lib/src/params/days_param.dart @@ -0,0 +1,37 @@ +part of '../../cron.dart'; + +class _DaysParam extends _CronParam { + List _days; + + /// This will add support for the `L` special char on the cron string + /// if `true` the [shouldTick] method will ignore the list of days and fire only on the last day of + /// the given moment + bool _onLastDayOnly = false; + + _DaysParam(dynamic days) : super() { + if (days == 'L') { + _onLastDayOnly = true; + } else { + _days = parseConstraint(days)?.where((x) => x >= 1 && x <= 31)?.toList(); + } + } + + @override + bool shouldTick(DateTime moment) { + if (_onLastDayOnly) { + /// Get the last day of the given moment + final lastDay = DateTime(moment.year, moment.month + 1, 0).day; + + return lastDay == moment.day; + } + + return _days?.contains(moment.day) ?? !_onLastDayOnly; + } + + @override + String toString() { + if (_onLastDayOnly) return 'Run only on last day of month'; + if (_days == null || _days.isEmpty) return 'Run everyday'; + return _days.toString(); + } +} diff --git a/lib/src/params/hours_param.dart b/lib/src/params/hours_param.dart new file mode 100644 index 0000000..92b4881 --- /dev/null +++ b/lib/src/params/hours_param.dart @@ -0,0 +1,18 @@ +part of '../../cron.dart'; + +class _HoursParam extends _CronParam { + List _hours; + + _HoursParam(dynamic hours) : super() { + _hours = parseConstraint(hours)?.where((x) => x >= 0 && x <= 59)?.toList(); + } + + @override + bool shouldTick(DateTime moment) => _hours?.contains(moment.hour) ?? true; + + @override + String toString() { + if (_hours == null || _hours.isEmpty) return 'Run every hours'; + return 'Run on hours $_hours'; + } +} diff --git a/lib/src/params/minutes_param.dart b/lib/src/params/minutes_param.dart new file mode 100644 index 0000000..b9b8e38 --- /dev/null +++ b/lib/src/params/minutes_param.dart @@ -0,0 +1,19 @@ +part of '../../cron.dart'; + +class _MinutesParam extends _CronParam { + List _minutes; + + _MinutesParam(dynamic minutes) : super() { + _minutes = + parseConstraint(minutes)?.where((x) => x >= 0 && x <= 59)?.toList(); + } + + @override + bool shouldTick(DateTime moment) => _minutes?.contains(moment.minute) ?? true; + + @override + String toString() { + if (_minutes == null || _minutes.isEmpty) return 'Run every minute'; + return 'Run on minutes $_minutes'; + } +} diff --git a/lib/src/params/months_param.dart b/lib/src/params/months_param.dart new file mode 100644 index 0000000..a5c1fd7 --- /dev/null +++ b/lib/src/params/months_param.dart @@ -0,0 +1,19 @@ +part of '../../cron.dart'; + +class _MonthsParam extends _CronParam { + List _months; + + _MonthsParam(dynamic months) : super() { + _months = + parseConstraint(months)?.where((x) => x >= 1 && x <= 12)?.toList(); + } + + @override + bool shouldTick(DateTime moment) => _months?.contains(moment.month) ?? true; + + @override + String toString() { + if (_months == null || _months.isEmpty) return 'Run every month'; + return 'Run on months $_months'; + } +} diff --git a/lib/src/params/seconds_param.dart b/lib/src/params/seconds_param.dart new file mode 100644 index 0000000..b687cc7 --- /dev/null +++ b/lib/src/params/seconds_param.dart @@ -0,0 +1,24 @@ +part of '../../cron.dart'; + +class _SecondsParam extends _CronParam { + List _seconds; + + _SecondsParam(dynamic seconds) : super() { + _seconds = + parseConstraint(seconds)?.where((x) => x >= 0 && x <= 59)?.toList(); + } + + @override + bool shouldTick(DateTime moment) => _seconds?.contains(moment.second) ?? true; + + bool get hasSeconds => + _seconds != null && + _seconds.isNotEmpty && + (_seconds.length != 1 || !_seconds.contains(0)); + + @override + String toString() { + if (_seconds == null || _seconds.isEmpty) return 'Run every second'; + return 'Run on seconds $_seconds'; + } +} diff --git a/lib/src/params/weekday_param.dart b/lib/src/params/weekday_param.dart new file mode 100644 index 0000000..b181b65 --- /dev/null +++ b/lib/src/params/weekday_param.dart @@ -0,0 +1,54 @@ +part of '../../cron.dart'; + +class _WeekdayParam extends _CronParam { + List _weekdays; + + /// If true it means that that this param should fire only on last day of the week + bool _onLastMonthWeekdayOnly; + + /// Works together with the [_onLastMonthWeekdayOnly] attribute, + /// so [_onLastMonthWeekdayOnly] must be true and [_onLastWeekDay] should have a value + /// between 0 and 7 + int _lastWeekDay = 6; + + _WeekdayParam(dynamic weekdays) : super() { + _onLastMonthWeekdayOnly = false; + + if (weekdays.trim() == 'L') { + _weekdays = [_lastWeekDay]; + } else if (weekdays is String && + weekdays.trim().startsWith(RegExp(r'[0-7]')) && + weekdays.endsWith('L')) { + _onLastMonthWeekdayOnly = true; + _lastWeekDay = int.tryParse(weekdays.trim()[0]); + } else { + _weekdays = parseConstraint(weekdays) + ?.where((x) => x >= 0 && x <= 7) + ?.map((x) => x == 0 ? 7 : x) + ?.toSet() + ?.toList(); + } + } + + @override + bool shouldTick(DateTime moment) { + /// If `_onLastWeekdayOnly` is true + /// and the weekday of the moment is the same as the requested last weekday param + if (_onLastMonthWeekdayOnly && moment.weekday == _lastWeekDay) { + /// Add a week to the current moment and check if the month is still the same as the moment + /// if not it means that this is the last weekday of the current moment + final nextWeekDate = DateTime(moment.year, moment.month, moment.day + 7); + return nextWeekDate.month != moment.month; + } + return _weekdays?.contains(moment.weekday) ?? !_onLastMonthWeekdayOnly; + } + + @override + String toString() { + if (_onLastMonthWeekdayOnly) return 'Run only on week day $_lastWeekDay'; + if (_weekdays == null || _weekdays.isEmpty) { + return 'Run at any day of the week'; + } + return 'Run on days of week $_weekdays'; + } +} diff --git a/lib/src/params/years_param.dart b/lib/src/params/years_param.dart new file mode 100644 index 0000000..4e09b9b --- /dev/null +++ b/lib/src/params/years_param.dart @@ -0,0 +1,21 @@ +part of '../../cron.dart'; + +class _YearsParam extends _CronParam { + List _years; + + _YearsParam(dynamic years) : super() { + final now = DateTime.now(); + _years = parseConstraint(years) + ?.where((x) => x >= now.year && x <= now.year + 100) + ?.toList(); + } + + @override + bool shouldTick(DateTime moment) => _years?.contains(moment.year) ?? true; + + @override + String toString() { + if (_years == null || _years.isEmpty) return 'Run at any year'; + return 'Run on years $_years'; + } +} diff --git a/lib/src/scheduler.dart b/lib/src/scheduler.dart new file mode 100644 index 0000000..21ea614 --- /dev/null +++ b/lib/src/scheduler.dart @@ -0,0 +1,42 @@ +part of '../cron.dart'; + +/// The cron schedule. +class Schedule { + final List<_CronParam> params; + + Schedule(this.params) + : assert(params != null && params.isNotEmpty && params.length == 7); + + /// Parses the cron-formatted text and creates a schedule out of it. + factory Schedule.parse(String cronFormat) { + final p = cronFormat.split(RegExp('\\s+')); + + assert([5, 6, 7].contains(p.length)); + final parts = [ + if (p.length == 5) null, + ...p, + if (p.length == 5 || p.length == 6) null, + ]; + + return Schedule([ + _SecondsParam(parts[0]), + _MinutesParam(parts[1]), + _HoursParam(parts[2]), + _DaysParam(parts[3]), + _MonthsParam(parts[4]), + _WeekdayParam(parts[5]), + _YearsParam(parts[6]), + ]); + } + + bool get _hasSeconds => + params[0] is _SecondsParam && (params[0] as _SecondsParam).hasSeconds; + + @override + String toString() => params.join('\n'); +} + +abstract class ScheduledTask { + Schedule get schedule; + Future cancel(); +} diff --git a/pubspec.yaml b/pubspec.yaml index 4fc3f0d..1accb20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: cron description: A time-based job scheduler similar to cron. Run tasks periodically at fixed times or intervals. -version: 0.2.4 +version: 0.2.5 homepage: https://github.com/agilord/cron environment: diff --git a/test/parser_test.dart b/test/parser_test.dart deleted file mode 100644 index 948115d..0000000 --- a/test/parser_test.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:cron/cron.dart'; -import 'package:test/test.dart'; - -import 'package:cron/src/constraint_parser.dart'; - -void main() { - group('Constraints', () { - test('parse fixed constraint', () { - expect(parseConstraint('12,25'), [12, 25]); - }); - - test('parse star constraint', () { - expect(parseConstraint('*/14'), [0, 14, 28, 42, 56, 70, 84, 98]); - }); - }); - - group('Schedule.parse', () { - test('5 parts', () { - final schedule = Schedule.parse('1 13 */2 3-6 *'); - expect(schedule.seconds, isNull); - expect(schedule.minutes, [1]); - expect(schedule.hours, [13]); - expect(schedule.days, - [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]); - expect(schedule.months, [3, 4, 5, 6]); - expect(schedule.weekdays, isNull); - }); - - test('6 parts', () { - final schedule = Schedule.parse('1,30-31 1 13 */2 3-6 2,4'); - expect(schedule.seconds, [1, 30, 31]); - expect(schedule.minutes, [1]); - expect(schedule.hours, [13]); - expect(schedule.days, - [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]); - expect(schedule.months, [3, 4, 5, 6]); - expect(schedule.weekdays, [2, 4]); - }); - }); -}