diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..fe017a6 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-reportgenerator-globaltool": { + "version": "5.1.22", + "commands": [ + "reportgenerator" + ], + "rollForward": false + }, + "meziantou.framework.nugetpackagevalidation.tool": { + "version": "1.0.16", + "commands": [ + "meziantou.validate-nuget-package" + ], + "rollForward": false + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fbe882f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,125 @@ +# http://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,props,targets}] +indent_size = 2 + +[*.{sln}] +charset = utf-8-bom +indent_style = tab + +[*.{json,yml,xml,runsettings}] +indent_size = 2 + +[*.{cs,tt}] +indent_style = space +indent_size = 4 +max_line_length = 100 + +[*.cs] +dotnet_analyzer_diagnostic.category-Style.severity = warning + +# IDE0022: Use expression/block body for methods +dotnet_diagnostic.IDE0022.severity = suggestion + +# IDE1006: Naming rule violation +dotnet_diagnostic.IDE1006.severity = warning + +# Naming capitalization styles +dotnet_naming_style.camel_case_style.capitalization = camel_case +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Naming rule that private instance fields must use camel case +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_rule.camel_case_private_fields.severity = warning +dotnet_naming_rule.camel_case_private_fields.symbols = private_fields +dotnet_naming_rule.camel_case_private_fields.style = camel_case_style + +# Naming rule that static read-only fields must use Pascal case +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = * +dotnet_naming_symbols.static_readonly_fields.required_modifiers = readonly, static +dotnet_naming_rule.pascal_case_static_readonly_fields.severity = warning +dotnet_naming_rule.pascal_case_static_readonly_fields.symbols = static_readonly_fields +dotnet_naming_rule.pascal_case_static_readonly_fields.style = pascal_case_style + +# Naming rule that const fields must use Pascal case +dotnet_naming_symbols.const_fields.applicable_kinds = field +dotnet_naming_symbols.const_fields.applicable_accessibilities = * +dotnet_naming_symbols.const_fields.required_modifiers = const +dotnet_naming_rule.pascal_case_const_fields.severity = warning +dotnet_naming_rule.pascal_case_const_fields.symbols = const_fields +dotnet_naming_rule.pascal_case_const_fields.style = pascal_case_style + +# this. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = true +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true +csharp_style_var_elsewhere = true + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = true +csharp_style_expression_bodied_constructors = true +csharp_style_expression_bodied_operators = true + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_accessors = true + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_inlined_variable_declaration = true +csharp_style_throw_expression = true +csharp_style_conditional_delegate_call = true +csharp_prefer_simple_default_expression = true + +# Spacing +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false + +# Wrapping +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +# Indentation +csharp_indent_case_contents_when_block = false + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = omit_if_default + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline + +# IDE0061: Use block body for local functions +csharp_style_expression_bodied_local_functions = true + +# IDE0065: Misplaced using directive +csharp_using_directive_placement = outside_namespace + +# IDE0048: Add parentheses for clarity +dotnet_diagnostic.IDE0048.severity = suggestion + +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = suggestion + +# IDE0046: Convert to conditional expression +dotnet_diagnostic.IDE0046.severity = suggestion + +# IDE0160: Convert to block scoped namespace +csharp_style_namespace_declarations = file_scoped diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..f040824 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# For more information on this file, see: +# https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revltrevgt + +# Convert to file-scoped namespace +da2b51af0c8186454c542dc34d17abb81e8c6dff diff --git a/.gitignore b/.gitignore index 0e46a6e..263ae55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,373 @@ -bin/ -*.dll -*.pdb -*.user -*.cache +# Created by https://www.gitignore.io/api/linux,macos,windows,visualstudio,visualstudiocode + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files *.suo -obj/ -obj.portable/ -_ReSharper.* -*.orig -dist/ -pkg/base/ -*.nupkg -packages/* +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory .vs/ -project.lock.json +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Uncomment the next line to ignore your web deploy settings. +# By default, sensitive information, such as encrypted password +# should be stored in the .pubxml.user file. +#*.pubxml +*.pubxml.user +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +### VisualStudio Patch ### +# By default, sensitive information, such as encrypted password +# should be stored in the .pubxml.user file. + +# End of https://www.gitignore.io/api/linux,macos,windows,visualstudio,visualstudiocode + +temp/ +tmp/ +etc/coverage/ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..4ddecab --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,12 @@ + + + 12 + true + 8.0-all + enable + true + true + + EnableGenerateDocumentationFile + + diff --git a/NCrontab.Signed/NCrontab.Signed.csproj b/NCrontab.Signed/NCrontab.Signed.csproj index 17e3bac..4f577ad 100644 --- a/NCrontab.Signed/NCrontab.Signed.csproj +++ b/NCrontab.Signed/NCrontab.Signed.csproj @@ -1,68 +1,19 @@ - + + + - NCrontab is crontab for all .NET runtimes supported by .NET Standard 1.0. It provides parsing and formatting of crontab expressions as well as calculation of occurrences of time based on a schedule expressed in the crontab format. - Copyright © 2008 Atif Aziz. All rights reserved. Portions Copyright © 2001 The OpenSymphony Group. All rights reserved. - NCrontab (Signed) - en-US - 3.3.2 - Atif Aziz - net35;netstandard1.0;netstandard2.0 - $(DefineConstants);SIGNED - true - portable NCrontab.Signed - Library + NCrontab (Signed) key.snk true true + $(DefineConstants);SIGNED NCrontab.Signed - cron;schedule;time - https://github.com/atifaziz/NCrontab - true - COPYING.txt - ..\dist - false - false - false - false - false - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - - - - - - - - $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client - - - - - - - - $(DefineConstants);SERIALIZATION - - - - - - - - - - - - - + diff --git a/NCrontab.Tests/.editorconfig b/NCrontab.Tests/.editorconfig new file mode 100644 index 0000000..6e96408 --- /dev/null +++ b/NCrontab.Tests/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org/ + +[*.cs] + +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = none + +# CA2201: Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = suggestion + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = none + +# CA1861: Avoid constant arrays as arguments +dotnet_diagnostic.CA1861.severity = suggestion diff --git a/NCrontab.Tests/.runsettings b/NCrontab.Tests/.runsettings new file mode 100644 index 0000000..cc74b6b --- /dev/null +++ b/NCrontab.Tests/.runsettings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/NCrontab.Tests/CrontabScheduleTests.cs b/NCrontab.Tests/CrontabScheduleTests.cs index 28d8c71..b35e68e 100644 --- a/NCrontab.Tests/CrontabScheduleTests.cs +++ b/NCrontab.Tests/CrontabScheduleTests.cs @@ -18,428 +18,489 @@ // #endregion -namespace NCrontab.Tests +using System; +using System.Globalization; +using System.Linq; +using NUnit.Framework; +using ParseOptions = NCrontab.CrontabSchedule.ParseOptions; + +namespace NCrontab.Tests; + +[TestFixture] +public sealed class CrontabScheduleTests { - using System; - using System.Globalization; - using System.Linq; - using NUnit.Framework; - using ParseOptions = CrontabSchedule.ParseOptions; - - [TestFixture] - public sealed class CrontabScheduleTests + const string TimeFormat = "dd/MM/yyyy HH:mm:ss"; + + static readonly string[] TimeFormats = + [ + "yyyy-MM-dd", + "yyyy-MM-dd HH:mm", + "yyyy-MM-dd HH:mm:ss", + "dd/MM/yyyy HH:mm:ss" + ]; + + [Test] + public void CannotParseNullString() { - const string TimeFormat = "dd/MM/yyyy HH:mm:ss"; + Assert.That(() => CrontabSchedule.Parse(null!), + Throws.ArgumentNullException + .With.Property(nameof(ArgumentNullException.ParamName)).EqualTo("expression")); + } - static readonly string[] TimeFormats = - { - "yyyy-MM-dd", - "yyyy-MM-dd HH:mm", - "yyyy-MM-dd HH:mm:ss", - "dd/MM/yyyy HH:mm:ss" - }; + [Test] + public void CannotParseEmptyString() + { + Assert.That(() => CrontabSchedule.Parse(string.Empty), Throws.TypeOf()); + } - [Test] - public void CannotParseNullString() - { - var e = Assert.Throws(() => CrontabSchedule.Parse(null)); - Assert.That(e.ParamName, Is.EqualTo("expression")); - } + [Test] + public void TryParseNullString() => + Assert.That(CrontabSchedule.TryParse(null!), Is.Null); - [Test] - public void CannotParseEmptyString() - { - Assert.Throws(() => CrontabSchedule.Parse(string.Empty)); - } + [Test] + public void TryParseEmptyString() => + Assert.That(CrontabSchedule.TryParse(string.Empty), Is.Null); - [Test] - public void TryParseNullString() => - Assert.That(CrontabSchedule.TryParse(null), Is.Null); + [Test] + public void AllTimeString() + { + var result = CrontabSchedule.Parse("* * * * *").ToString(); + Assert.That(result, Is.EqualTo("* * * * *")); + } - [Test] - public void TryParseEmptyString() => - Assert.That(CrontabSchedule.TryParse(string.Empty), Is.Null); + [Test] + public void SixPartAllTimeString() + { + var result = CrontabSchedule.Parse("* * * * * *", new ParseOptions { IncludingSeconds = true }).ToString(); + Assert.That(result, Is.EqualTo("* * * * * *")); + } - [Test] - public void AllTimeString() - { - Assert.AreEqual("* * * * *", CrontabSchedule.Parse("* * * * *").ToString()); - } + [Test] + public void CannotParseWhenSecondsRequired() + { + Assert.That(() => CrontabSchedule.Parse("* * * * *", new ParseOptions { IncludingSeconds = true }), + Throws.TypeOf()); + } - [Test] - public void SixPartAllTimeString() - { - Assert.AreEqual("* * * * * *", CrontabSchedule.Parse("* * * * * *", new ParseOptions { IncludingSeconds = true }).ToString()); - } + [TestCase("* 1-3 * * *" , "* 1-2,3 * * *" , false)] + [TestCase("* * * 1,3,5,7,9,11 *" , "* * * */2 *" , false)] + [TestCase("10,25,40 * * * *" , "10-40/15 * * * *" , false)] + [TestCase("* * * 1,3,8 1-2,5" , "* * * Mar,Jan,Aug Fri,Mon-Tue" , false)] + [TestCase("1 * 1-3 * * *" , "1 * 1-2,3 * * *" , true )] + [TestCase("22 * * * 1,3,5,7,9,11 *", "22 * * * */2 *" , true )] + [TestCase("33 10,25,40 * * * *" , "33 10-40/15 * * * *" , true )] + [TestCase("55 * * * 1,3,8 1-2,5" , "55 * * * Mar,Jan,Aug Fri,Mon-Tue", true )] + public void Formatting(string format, string expression, bool includingSeconds) + { + var options = new ParseOptions { IncludingSeconds = includingSeconds }; + var result = CrontabSchedule.Parse(expression, options).ToString(); + Assert.That(result, Is.EqualTo(format)); + } - [Test] - public void CannotParseWhenSecondsRequired() - { - Assert.Throws(() => CrontabSchedule.Parse("* * * * *", new ParseOptions { IncludingSeconds = true })); - } - - [TestCase("* 1-3 * * *" , "* 1-2,3 * * *" , false)] - [TestCase("* * * 1,3,5,7,9,11 *" , "* * * */2 *" , false)] - [TestCase("10,25,40 * * * *" , "10-40/15 * * * *" , false)] - [TestCase("* * * 1,3,8 1-2,5" , "* * * Mar,Jan,Aug Fri,Mon-Tue" , false)] - [TestCase("1 * 1-3 * * *" , "1 * 1-2,3 * * *" , true )] - [TestCase("22 * * * 1,3,5,7,9,11 *", "22 * * * */2 *" , true )] - [TestCase("33 10,25,40 * * * *" , "33 10-40/15 * * * *" , true )] - [TestCase("55 * * * 1,3,8 1-2,5" , "55 * * * Mar,Jan,Aug Fri,Mon-Tue", true )] - public void Formatting(string format, string expression, bool includingSeconds) - { - var options = new ParseOptions { IncludingSeconds = includingSeconds }; - Assert.AreEqual(format, CrontabSchedule.Parse(expression, options).ToString()); - } - - /// - /// Tests to see if the cron class can calculate the previous matching - /// time correctly in various circumstances. - /// + /// + /// Tests to see if the cron class can calculate the previous matching + /// time correctly in various circumstances. + /// - [TestCase("01/01/2003 00:00:00", "* * * * *", "01/01/2003 00:01:00", false)] - [TestCase("01/01/2003 00:01:00", "* * * * *", "01/01/2003 00:02:00", false)] - [TestCase("01/01/2003 00:02:00", "* * * * *", "01/01/2003 00:03:00", false)] - [TestCase("01/01/2003 00:59:00", "* * * * *", "01/01/2003 01:00:00", false)] - [TestCase("01/01/2003 01:59:00", "* * * * *", "01/01/2003 02:00:00", false)] - [TestCase("01/01/2003 23:59:00", "* * * * *", "02/01/2003 00:00:00", false)] - [TestCase("31/12/2003 23:59:00", "* * * * *", "01/01/2004 00:00:00", false)] + [TestCase("01/01/2003 00:00:00", "* * * * *", "01/01/2003 00:01:00", false)] + [TestCase("01/01/2003 00:01:00", "* * * * *", "01/01/2003 00:02:00", false)] + [TestCase("01/01/2003 00:02:00", "* * * * *", "01/01/2003 00:03:00", false)] + [TestCase("01/01/2003 00:59:00", "* * * * *", "01/01/2003 01:00:00", false)] + [TestCase("01/01/2003 01:59:00", "* * * * *", "01/01/2003 02:00:00", false)] + [TestCase("01/01/2003 23:59:00", "* * * * *", "02/01/2003 00:00:00", false)] + [TestCase("31/12/2003 23:59:00", "* * * * *", "01/01/2004 00:00:00", false)] - [TestCase("28/02/2003 23:59:00", "* * * * *", "01/03/2003 00:00:00", false)] - [TestCase("28/02/2004 23:59:00", "* * * * *", "29/02/2004 00:00:00", false)] + [TestCase("28/02/2003 23:59:00", "* * * * *", "01/03/2003 00:00:00", false)] + [TestCase("28/02/2004 23:59:00", "* * * * *", "29/02/2004 00:00:00", false)] - // Second tests + // Second tests - [TestCase("01/01/2003 00:00:00", "45 * * * * *", "01/01/2003 00:00:45" , true)] + [TestCase("01/01/2003 00:00:00", "45 * * * * *", "01/01/2003 00:00:45" , true)] - [TestCase("01/01/2003 00:00:00", "45-47,48,49 * * * * *", "01/01/2003 00:00:45", true)] - [TestCase("01/01/2003 00:00:45", "45-47,48,49 * * * * *", "01/01/2003 00:00:46", true)] - [TestCase("01/01/2003 00:00:46", "45-47,48,49 * * * * *", "01/01/2003 00:00:47", true)] - [TestCase("01/01/2003 00:00:47", "45-47,48,49 * * * * *", "01/01/2003 00:00:48", true)] - [TestCase("01/01/2003 00:00:48", "45-47,48,49 * * * * *", "01/01/2003 00:00:49", true)] - [TestCase("01/01/2003 00:00:49", "45-47,48,49 * * * * *", "01/01/2003 00:01:45", true)] + [TestCase("01/01/2003 00:00:00", "45-47,48,49 * * * * *", "01/01/2003 00:00:45", true)] + [TestCase("01/01/2003 00:00:45", "45-47,48,49 * * * * *", "01/01/2003 00:00:46", true)] + [TestCase("01/01/2003 00:00:46", "45-47,48,49 * * * * *", "01/01/2003 00:00:47", true)] + [TestCase("01/01/2003 00:00:47", "45-47,48,49 * * * * *", "01/01/2003 00:00:48", true)] + [TestCase("01/01/2003 00:00:48", "45-47,48,49 * * * * *", "01/01/2003 00:00:49", true)] + [TestCase("01/01/2003 00:00:49", "45-47,48,49 * * * * *", "01/01/2003 00:01:45", true)] - [TestCase("01/01/2003 00:00:00", "2/5 * * * * *", "01/01/2003 00:00:02" , true)] - [TestCase("01/01/2003 00:00:02", "2/5 * * * * *", "01/01/2003 00:00:07" , true)] - [TestCase("01/01/2003 00:00:50", "2/5 * * * * *", "01/01/2003 00:00:52" , true)] - [TestCase("01/01/2003 00:00:52", "2/5 * * * * *", "01/01/2003 00:00:57" , true)] - [TestCase("01/01/2003 00:00:57", "2/5 * * * * *", "01/01/2003 00:01:02" , true)] + [TestCase("01/01/2003 00:00:00", "2/5 * * * * *", "01/01/2003 00:00:02" , true)] + [TestCase("01/01/2003 00:00:02", "2/5 * * * * *", "01/01/2003 00:00:07" , true)] + [TestCase("01/01/2003 00:00:50", "2/5 * * * * *", "01/01/2003 00:00:52" , true)] + [TestCase("01/01/2003 00:00:52", "2/5 * * * * *", "01/01/2003 00:00:57" , true)] + [TestCase("01/01/2003 00:00:57", "2/5 * * * * *", "01/01/2003 00:01:02" , true)] - // Minute tests + // See: https://github.com/atifaziz/NCrontab/issues/90 + [TestCase("24/02/2021 09:50:35", "* 0-1 10 * * *", "24/02/2021 10:00:00" , true)] - [TestCase("01/01/2003 00:00:00", "45 * * * *", "01/01/2003 00:45:00", false)] + // Minute tests - [TestCase("01/01/2003 00:00:00", "45-47,48,49 * * * *", "01/01/2003 00:45:00", false)] - [TestCase("01/01/2003 00:45:00", "45-47,48,49 * * * *", "01/01/2003 00:46:00", false)] - [TestCase("01/01/2003 00:46:00", "45-47,48,49 * * * *", "01/01/2003 00:47:00", false)] - [TestCase("01/01/2003 00:47:00", "45-47,48,49 * * * *", "01/01/2003 00:48:00", false)] - [TestCase("01/01/2003 00:48:00", "45-47,48,49 * * * *", "01/01/2003 00:49:00", false)] - [TestCase("01/01/2003 00:49:00", "45-47,48,49 * * * *", "01/01/2003 01:45:00", false)] - - [TestCase("01/01/2003 00:00:00", "2/5 * * * *", "01/01/2003 00:02:00", false)] - [TestCase("01/01/2003 00:02:00", "2/5 * * * *", "01/01/2003 00:07:00", false)] - [TestCase("01/01/2003 00:50:00", "2/5 * * * *", "01/01/2003 00:52:00", false)] - [TestCase("01/01/2003 00:52:00", "2/5 * * * *", "01/01/2003 00:57:00", false)] - [TestCase("01/01/2003 00:57:00", "2/5 * * * *", "01/01/2003 01:02:00", false)] + [TestCase("01/01/2003 00:00:00", "45 * * * *", "01/01/2003 00:45:00", false)] - [TestCase("01/01/2003 00:00:30", "3 45 * * * *", "01/01/2003 00:45:03", true)] - - [TestCase("01/01/2003 00:00:30", "6 45-47,48,49 * * * *", "01/01/2003 00:45:06", true)] - [TestCase("01/01/2003 00:45:30", "6 45-47,48,49 * * * *", "01/01/2003 00:46:06", true)] - [TestCase("01/01/2003 00:46:30", "6 45-47,48,49 * * * *", "01/01/2003 00:47:06", true)] - [TestCase("01/01/2003 00:47:30", "6 45-47,48,49 * * * *", "01/01/2003 00:48:06", true)] - [TestCase("01/01/2003 00:48:30", "6 45-47,48,49 * * * *", "01/01/2003 00:49:06", true)] - [TestCase("01/01/2003 00:49:30", "6 45-47,48,49 * * * *", "01/01/2003 01:45:06", true)] - - [TestCase("01/01/2003 00:00:30", "9 2/5 * * * *", "01/01/2003 00:02:09", true)] - [TestCase("01/01/2003 00:02:30", "9 2/5 * * * *", "01/01/2003 00:07:09", true)] - [TestCase("01/01/2003 00:50:30", "9 2/5 * * * *", "01/01/2003 00:52:09", true)] - [TestCase("01/01/2003 00:52:30", "9 2/5 * * * *", "01/01/2003 00:57:09", true)] - [TestCase("01/01/2003 00:57:30", "9 2/5 * * * *", "01/01/2003 01:02:09", true)] - - // Hour tests + [TestCase("01/01/2003 00:00:00", "45-47,48,49 * * * *", "01/01/2003 00:45:00", false)] + [TestCase("01/01/2003 00:45:00", "45-47,48,49 * * * *", "01/01/2003 00:46:00", false)] + [TestCase("01/01/2003 00:46:00", "45-47,48,49 * * * *", "01/01/2003 00:47:00", false)] + [TestCase("01/01/2003 00:47:00", "45-47,48,49 * * * *", "01/01/2003 00:48:00", false)] + [TestCase("01/01/2003 00:48:00", "45-47,48,49 * * * *", "01/01/2003 00:49:00", false)] + [TestCase("01/01/2003 00:49:00", "45-47,48,49 * * * *", "01/01/2003 01:45:00", false)] - [TestCase("20/12/2003 10:00:00", " * 3/4 * * *", "20/12/2003 11:00:00", false)] - [TestCase("20/12/2003 00:30:00", " * 3 * * *", "20/12/2003 03:00:00", false)] - [TestCase("20/12/2003 01:45:00", "30 3 * * *", "20/12/2003 03:30:00", false)] + [TestCase("01/01/2003 00:00:00", "2/5 * * * *", "01/01/2003 00:02:00", false)] + [TestCase("01/01/2003 00:02:00", "2/5 * * * *", "01/01/2003 00:07:00", false)] + [TestCase("01/01/2003 00:50:00", "2/5 * * * *", "01/01/2003 00:52:00", false)] + [TestCase("01/01/2003 00:52:00", "2/5 * * * *", "01/01/2003 00:57:00", false)] + [TestCase("01/01/2003 00:57:00", "2/5 * * * *", "01/01/2003 01:02:00", false)] - // Day of month tests + // See: https://github.com/atifaziz/NCrontab/issues/90 + [TestCase("24/02/2021 09:50:35", "* * 10 * * *", "24/02/2021 10:00:00", true)] + [TestCase("24/02/2021 09:50:35", "* 55 * * * *", "24/02/2021 09:55:00", true)] - [TestCase("07/01/2003 00:00:00", "30 * 1 * *", "01/02/2003 00:30:00", false)] - [TestCase("01/02/2003 00:30:00", "30 * 1 * *", "01/02/2003 01:30:00", false)] - - [TestCase("01/01/2003 00:00:00", "10 * 22 * *", "22/01/2003 00:10:00", false)] - [TestCase("01/01/2003 00:00:00", "30 23 19 * *", "19/01/2003 23:30:00", false)] - [TestCase("01/01/2003 00:00:00", "30 23 21 * *", "21/01/2003 23:30:00", false)] - [TestCase("01/01/2003 00:01:00", " * * 21 * *", "21/01/2003 00:00:00", false)] - [TestCase("10/07/2003 00:00:00", " * * 30,31 * *", "30/07/2003 00:00:00", false)] + [TestCase("01/01/2003 00:00:30", "3 45 * * * *", "01/01/2003 00:45:03", true)] - // Test month rollovers for months with 28,29,30 and 31 days + [TestCase("01/01/2003 00:00:30", "6 45-47,48,49 * * * *", "01/01/2003 00:45:06", true)] + [TestCase("01/01/2003 00:45:30", "6 45-47,48,49 * * * *", "01/01/2003 00:46:06", true)] + [TestCase("01/01/2003 00:46:30", "6 45-47,48,49 * * * *", "01/01/2003 00:47:06", true)] + [TestCase("01/01/2003 00:47:30", "6 45-47,48,49 * * * *", "01/01/2003 00:48:06", true)] + [TestCase("01/01/2003 00:48:30", "6 45-47,48,49 * * * *", "01/01/2003 00:49:06", true)] + [TestCase("01/01/2003 00:49:30", "6 45-47,48,49 * * * *", "01/01/2003 01:45:06", true)] - [TestCase("28/02/2002 23:59:59", "* * * 3 *", "01/03/2002 00:00:00", false)] - [TestCase("29/02/2004 23:59:59", "* * * 3 *", "01/03/2004 00:00:00", false)] - [TestCase("31/03/2002 23:59:59", "* * * 4 *", "01/04/2002 00:00:00", false)] - [TestCase("30/04/2002 23:59:59", "* * * 5 *", "01/05/2002 00:00:00", false)] + [TestCase("01/01/2003 00:00:30", "9 2/5 * * * *", "01/01/2003 00:02:09", true)] + [TestCase("01/01/2003 00:02:30", "9 2/5 * * * *", "01/01/2003 00:07:09", true)] + [TestCase("01/01/2003 00:50:30", "9 2/5 * * * *", "01/01/2003 00:52:09", true)] + [TestCase("01/01/2003 00:52:30", "9 2/5 * * * *", "01/01/2003 00:57:09", true)] + [TestCase("01/01/2003 00:57:30", "9 2/5 * * * *", "01/01/2003 01:02:09", true)] - // Test month 30,31 days + // Hour tests - [TestCase("01/01/2000 00:00:00", "0 0 15,30,31 * *", "15/01/2000 00:00:00", false)] - [TestCase("15/01/2000 00:00:00", "0 0 15,30,31 * *", "30/01/2000 00:00:00", false)] - [TestCase("30/01/2000 00:00:00", "0 0 15,30,31 * *", "31/01/2000 00:00:00", false)] - [TestCase("31/01/2000 00:00:00", "0 0 15,30,31 * *", "15/02/2000 00:00:00", false)] + [TestCase("20/12/2003 10:00:00", " * 3/4 * * *", "20/12/2003 11:00:00", false)] + [TestCase("20/12/2003 00:30:00", " * 3 * * *", "20/12/2003 03:00:00", false)] + [TestCase("20/12/2003 01:45:00", "30 3 * * *", "20/12/2003 03:30:00", false)] - [TestCase("15/02/2000 00:00:00", "0 0 15,30,31 * *", "15/03/2000 00:00:00", false)] + // Day of month tests - [TestCase("15/03/2000 00:00:00", "0 0 15,30,31 * *", "30/03/2000 00:00:00", false)] - [TestCase("30/03/2000 00:00:00", "0 0 15,30,31 * *", "31/03/2000 00:00:00", false)] - [TestCase("31/03/2000 00:00:00", "0 0 15,30,31 * *", "15/04/2000 00:00:00", false)] + [TestCase("07/01/2003 00:00:00", "30 * 1 * *", "01/02/2003 00:30:00", false)] + [TestCase("01/02/2003 00:30:00", "30 * 1 * *", "01/02/2003 01:30:00", false)] - [TestCase("15/04/2000 00:00:00", "0 0 15,30,31 * *", "30/04/2000 00:00:00", false)] - [TestCase("30/04/2000 00:00:00", "0 0 15,30,31 * *", "15/05/2000 00:00:00", false)] + [TestCase("01/01/2003 00:00:00", "10 * 22 * *", "22/01/2003 00:10:00", false)] + [TestCase("01/01/2003 00:00:00", "30 23 19 * *", "19/01/2003 23:30:00", false)] + [TestCase("01/01/2003 00:00:00", "30 23 21 * *", "21/01/2003 23:30:00", false)] + [TestCase("01/01/2003 00:01:00", " * * 21 * *", "21/01/2003 00:00:00", false)] + [TestCase("10/07/2003 00:00:00", " * * 30,31 * *", "30/07/2003 00:00:00", false)] - [TestCase("15/05/2000 00:00:00", "0 0 15,30,31 * *", "30/05/2000 00:00:00", false)] - [TestCase("30/05/2000 00:00:00", "0 0 15,30,31 * *", "31/05/2000 00:00:00", false)] - [TestCase("31/05/2000 00:00:00", "0 0 15,30,31 * *", "15/06/2000 00:00:00", false)] + // Test month rollovers for months with 28,29,30 and 31 days - [TestCase("15/06/2000 00:00:00", "0 0 15,30,31 * *", "30/06/2000 00:00:00", false)] - [TestCase("30/06/2000 00:00:00", "0 0 15,30,31 * *", "15/07/2000 00:00:00", false)] + [TestCase("28/02/2002 23:59:59", "* * * 3 *", "01/03/2002 00:00:00", false)] + [TestCase("29/02/2004 23:59:59", "* * * 3 *", "01/03/2004 00:00:00", false)] + [TestCase("31/03/2002 23:59:59", "* * * 4 *", "01/04/2002 00:00:00", false)] + [TestCase("30/04/2002 23:59:59", "* * * 5 *", "01/05/2002 00:00:00", false)] - [TestCase("15/07/2000 00:00:00", "0 0 15,30,31 * *", "30/07/2000 00:00:00", false)] - [TestCase("30/07/2000 00:00:00", "0 0 15,30,31 * *", "31/07/2000 00:00:00", false)] - [TestCase("31/07/2000 00:00:00", "0 0 15,30,31 * *", "15/08/2000 00:00:00", false)] + // Test month 30,31 days - [TestCase("15/08/2000 00:00:00", "0 0 15,30,31 * *", "30/08/2000 00:00:00", false)] - [TestCase("30/08/2000 00:00:00", "0 0 15,30,31 * *", "31/08/2000 00:00:00", false)] - [TestCase("31/08/2000 00:00:00", "0 0 15,30,31 * *", "15/09/2000 00:00:00", false)] + [TestCase("01/01/2000 00:00:00", "0 0 15,30,31 * *", "15/01/2000 00:00:00", false)] + [TestCase("15/01/2000 00:00:00", "0 0 15,30,31 * *", "30/01/2000 00:00:00", false)] + [TestCase("30/01/2000 00:00:00", "0 0 15,30,31 * *", "31/01/2000 00:00:00", false)] + [TestCase("31/01/2000 00:00:00", "0 0 15,30,31 * *", "15/02/2000 00:00:00", false)] - [TestCase("15/09/2000 00:00:00", "0 0 15,30,31 * *", "30/09/2000 00:00:00", false)] - [TestCase("30/09/2000 00:00:00", "0 0 15,30,31 * *", "15/10/2000 00:00:00", false)] + [TestCase("15/02/2000 00:00:00", "0 0 15,30,31 * *", "15/03/2000 00:00:00", false)] - [TestCase("15/10/2000 00:00:00", "0 0 15,30,31 * *", "30/10/2000 00:00:00", false)] - [TestCase("30/10/2000 00:00:00", "0 0 15,30,31 * *", "31/10/2000 00:00:00", false)] - [TestCase("31/10/2000 00:00:00", "0 0 15,30,31 * *", "15/11/2000 00:00:00", false)] + [TestCase("15/03/2000 00:00:00", "0 0 15,30,31 * *", "30/03/2000 00:00:00", false)] + [TestCase("30/03/2000 00:00:00", "0 0 15,30,31 * *", "31/03/2000 00:00:00", false)] + [TestCase("31/03/2000 00:00:00", "0 0 15,30,31 * *", "15/04/2000 00:00:00", false)] - [TestCase("15/11/2000 00:00:00", "0 0 15,30,31 * *", "30/11/2000 00:00:00", false)] - [TestCase("30/11/2000 00:00:00", "0 0 15,30,31 * *", "15/12/2000 00:00:00", false)] + [TestCase("15/04/2000 00:00:00", "0 0 15,30,31 * *", "30/04/2000 00:00:00", false)] + [TestCase("30/04/2000 00:00:00", "0 0 15,30,31 * *", "15/05/2000 00:00:00", false)] - [TestCase("15/12/2000 00:00:00", "0 0 15,30,31 * *", "30/12/2000 00:00:00", false)] - [TestCase("30/12/2000 00:00:00", "0 0 15,30,31 * *", "31/12/2000 00:00:00", false)] - [TestCase("31/12/2000 00:00:00", "0 0 15,30,31 * *", "15/01/2001 00:00:00", false)] + [TestCase("15/05/2000 00:00:00", "0 0 15,30,31 * *", "30/05/2000 00:00:00", false)] + [TestCase("30/05/2000 00:00:00", "0 0 15,30,31 * *", "31/05/2000 00:00:00", false)] + [TestCase("31/05/2000 00:00:00", "0 0 15,30,31 * *", "15/06/2000 00:00:00", false)] - // Other month tests (including year rollover) + [TestCase("15/06/2000 00:00:00", "0 0 15,30,31 * *", "30/06/2000 00:00:00", false)] + [TestCase("30/06/2000 00:00:00", "0 0 15,30,31 * *", "15/07/2000 00:00:00", false)] - [TestCase("01/12/2003 05:00:00", "10 * * 6 *", "01/06/2004 00:10:00", false)] - [TestCase("04/01/2003 00:00:00", " 1 2 3 * *", "03/02/2003 02:01:00", false)] - [TestCase("01/07/2002 05:00:00", "10 * * February,April-Jun *", "01/02/2003 00:10:00", false)] - [TestCase("01/01/2003 00:00:00", "0 12 1 6 *", "01/06/2003 12:00:00", false)] - [TestCase("11/09/1988 14:23:00", "* 12 1 6 *", "01/06/1989 12:00:00", false)] - [TestCase("11/03/1988 14:23:00", "* 12 1 6 *", "01/06/1988 12:00:00", false)] - [TestCase("11/03/1988 14:23:00", "* 2,4-8,15 * 6 *", "01/06/1988 02:00:00", false)] - [TestCase("11/03/1988 14:23:00", "20 * * january,FeB,Mar,april,May,JuNE,July,Augu,SEPT-October,Nov,DECEM *", "11/03/1988 15:20:00", false)] + [TestCase("15/07/2000 00:00:00", "0 0 15,30,31 * *", "30/07/2000 00:00:00", false)] + [TestCase("30/07/2000 00:00:00", "0 0 15,30,31 * *", "31/07/2000 00:00:00", false)] + [TestCase("31/07/2000 00:00:00", "0 0 15,30,31 * *", "15/08/2000 00:00:00", false)] - // Day of week tests + [TestCase("15/08/2000 00:00:00", "0 0 15,30,31 * *", "30/08/2000 00:00:00", false)] + [TestCase("30/08/2000 00:00:00", "0 0 15,30,31 * *", "31/08/2000 00:00:00", false)] + [TestCase("31/08/2000 00:00:00", "0 0 15,30,31 * *", "15/09/2000 00:00:00", false)] - [TestCase("26/06/2003 10:00:00", "30 6 * * 0", "29/06/2003 06:30:00", false)] - [TestCase("26/06/2003 10:00:00", "30 6 * * sunday", "29/06/2003 06:30:00", false)] - [TestCase("26/06/2003 10:00:00", "30 6 * * SUNDAY", "29/06/2003 06:30:00", false)] - [TestCase("19/06/2003 00:00:00", "1 12 * * 2", "24/06/2003 12:01:00", false)] - [TestCase("24/06/2003 12:01:00", "1 12 * * 2", "01/07/2003 12:01:00", false)] + [TestCase("15/09/2000 00:00:00", "0 0 15,30,31 * *", "30/09/2000 00:00:00", false)] + [TestCase("30/09/2000 00:00:00", "0 0 15,30,31 * *", "15/10/2000 00:00:00", false)] - [TestCase("01/06/2003 14:55:00", "15 18 * * Mon", "02/06/2003 18:15:00", false)] - [TestCase("02/06/2003 18:15:00", "15 18 * * Mon", "09/06/2003 18:15:00", false)] - [TestCase("09/06/2003 18:15:00", "15 18 * * Mon", "16/06/2003 18:15:00", false)] - [TestCase("16/06/2003 18:15:00", "15 18 * * Mon", "23/06/2003 18:15:00", false)] - [TestCase("23/06/2003 18:15:00", "15 18 * * Mon", "30/06/2003 18:15:00", false)] - [TestCase("30/06/2003 18:15:00", "15 18 * * Mon", "07/07/2003 18:15:00", false)] + [TestCase("15/10/2000 00:00:00", "0 0 15,30,31 * *", "30/10/2000 00:00:00", false)] + [TestCase("30/10/2000 00:00:00", "0 0 15,30,31 * *", "31/10/2000 00:00:00", false)] + [TestCase("31/10/2000 00:00:00", "0 0 15,30,31 * *", "15/11/2000 00:00:00", false)] - [TestCase("01/01/2003 00:00:00", "* * * * Mon", "06/01/2003 00:00:00", false)] - [TestCase("01/01/2003 12:00:00", "45 16 1 * Mon", "01/09/2003 16:45:00", false)] - [TestCase("01/09/2003 23:45:00", "45 16 1 * Mon", "01/12/2003 16:45:00", false)] + [TestCase("15/11/2000 00:00:00", "0 0 15,30,31 * *", "30/11/2000 00:00:00", false)] + [TestCase("30/11/2000 00:00:00", "0 0 15,30,31 * *", "15/12/2000 00:00:00", false)] - // Leap year tests + [TestCase("15/12/2000 00:00:00", "0 0 15,30,31 * *", "30/12/2000 00:00:00", false)] + [TestCase("30/12/2000 00:00:00", "0 0 15,30,31 * *", "31/12/2000 00:00:00", false)] + [TestCase("31/12/2000 00:00:00", "0 0 15,30,31 * *", "15/01/2001 00:00:00", false)] - [TestCase("01/01/2000 12:00:00", "1 12 29 2 *", "29/02/2000 12:01:00", false)] - [TestCase("29/02/2000 12:01:00", "1 12 29 2 *", "29/02/2004 12:01:00", false)] - [TestCase("29/02/2004 12:01:00", "1 12 29 2 *", "29/02/2008 12:01:00", false)] + // Other month tests (including year rollover) - // Non-leap year tests + [TestCase("01/12/2003 05:00:00", "10 * * 6 *", "01/06/2004 00:10:00", false)] + [TestCase("04/01/2003 00:00:00", " 1 2 3 * *", "03/02/2003 02:01:00", false)] + [TestCase("01/07/2002 05:00:00", "10 * * February,April-Jun *", "01/02/2003 00:10:00", false)] + [TestCase("01/01/2003 00:00:00", "0 12 1 6 *", "01/06/2003 12:00:00", false)] + [TestCase("11/09/1988 14:23:00", "* 12 1 6 *", "01/06/1989 12:00:00", false)] + [TestCase("11/03/1988 14:23:00", "* 12 1 6 *", "01/06/1988 12:00:00", false)] + [TestCase("11/03/1988 14:23:00", "* 2,4-8,15 * 6 *", "01/06/1988 02:00:00", false)] + [TestCase("11/03/1988 14:23:00", "20 * * january,FeB,Mar,april,May,JuNE,July,Augu,SEPT-October,Nov,DECEM *", "11/03/1988 15:20:00", false)] - [TestCase("01/01/2000 12:00:00", "1 12 28 2 *", "28/02/2000 12:01:00", false)] - [TestCase("28/02/2000 12:01:00", "1 12 28 2 *", "28/02/2001 12:01:00", false)] - [TestCase("28/02/2001 12:01:00", "1 12 28 2 *", "28/02/2002 12:01:00", false)] - [TestCase("28/02/2002 12:01:00", "1 12 28 2 *", "28/02/2003 12:01:00", false)] - [TestCase("28/02/2003 12:01:00", "1 12 28 2 *", "28/02/2004 12:01:00", false)] - [TestCase("29/02/2004 12:01:00", "1 12 28 2 *", "28/02/2005 12:01:00", false)] + // Day of week tests - [TestCase("01/01/2000 12:00:00", "40 14/1 * * *", "01/01/2000 14:40:00", false)] - [TestCase("01/01/2000 14:40:00", "40 14/1 * * *", "01/01/2000 15:40:00", false)] + [TestCase("26/06/2003 10:00:00", "30 6 * * 0", "29/06/2003 06:30:00", false)] + [TestCase("26/06/2003 10:00:00", "30 6 * * sunday", "29/06/2003 06:30:00", false)] + [TestCase("26/06/2003 10:00:00", "30 6 * * SUNDAY", "29/06/2003 06:30:00", false)] + [TestCase("19/06/2003 00:00:00", "1 12 * * 2", "24/06/2003 12:01:00", false)] + [TestCase("24/06/2003 12:01:00", "1 12 * * 2", "01/07/2003 12:01:00", false)] - public void Evaluations(string startTimeString, string cronExpression, string nextTimeString, bool includingSeconds) - { - CronCall(startTimeString, cronExpression, nextTimeString, new ParseOptions - { - IncludingSeconds = includingSeconds - }); - } - - [TestCase(" * * * * * ", "01/01/2003 00:00:00", "01/01/2003 00:00:00" , false)] - [TestCase(" * * * * * ", "31/12/2002 23:59:59", "01/01/2003 00:00:00" , false)] - [TestCase(" * * * * Mon", "31/12/2002 23:59:59", "01/01/2003 00:00:00" , false)] - [TestCase(" * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 00:00:00" , false)] - [TestCase(" * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 12:00:00" , false)] - [TestCase("30 12 * * Mon", "01/01/2003 00:00:00", "06/01/2003 12:00:00" , false)] - - [TestCase(" * * * * * * ", "01/01/2003 00:00:00", "01/01/2003 00:00:00", true )] - [TestCase(" * * * * * * ", "31/12/2002 23:59:59", "01/01/2003 00:00:00", true )] - [TestCase(" * * * * * Mon", "31/12/2002 23:59:59", "01/01/2003 00:00:00", true )] - [TestCase(" * * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 00:00:00", true )] - [TestCase(" * * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 12:00:00", true )] - [TestCase("10 30 12 * * Mon", "01/01/2003 00:00:00", "06/01/2003 12:00:10", true )] - - public void FiniteOccurrences(string cronExpression, string startTimeString, string endTimeString, bool includingSeconds) - { - CronFinite(cronExpression, startTimeString, endTimeString, new ParseOptions - { - IncludingSeconds = includingSeconds - }); - } - - // - // Test to check we don't loop indefinitely looking for a February - // 31st because no such date would ever exist! - // - - [Category("Performance")] -#if NETCOREAPP1_0 - [Ignore("Timeout attribute missing from NUnit for .NET Core.")] -#else - [Timeout(1000)] -#endif - [TestCase("* * 31 Feb *", false)] - [TestCase("* * * 31 Feb *", true)] - public void DontLoopIndefinitely(string expression, bool includingSeconds) + [TestCase("01/06/2003 14:55:00", "15 18 * * Mon", "02/06/2003 18:15:00", false)] + [TestCase("02/06/2003 18:15:00", "15 18 * * Mon", "09/06/2003 18:15:00", false)] + [TestCase("09/06/2003 18:15:00", "15 18 * * Mon", "16/06/2003 18:15:00", false)] + [TestCase("16/06/2003 18:15:00", "15 18 * * Mon", "23/06/2003 18:15:00", false)] + [TestCase("23/06/2003 18:15:00", "15 18 * * Mon", "30/06/2003 18:15:00", false)] + [TestCase("30/06/2003 18:15:00", "15 18 * * Mon", "07/07/2003 18:15:00", false)] + + [TestCase("01/01/2003 00:00:00", "* * * * Mon", "06/01/2003 00:00:00", false)] + [TestCase("01/01/2003 12:00:00", "45 16 1 * Mon", "01/09/2003 16:45:00", false)] + [TestCase("01/09/2003 23:45:00", "45 16 1 * Mon", "01/12/2003 16:45:00", false)] + + // Leap year tests + + [TestCase("01/01/2000 12:00:00", "1 12 29 2 *", "29/02/2000 12:01:00", false)] + [TestCase("29/02/2000 12:01:00", "1 12 29 2 *", "29/02/2004 12:01:00", false)] + [TestCase("29/02/2004 12:01:00", "1 12 29 2 *", "29/02/2008 12:01:00", false)] + + // Non-leap year tests + + [TestCase("01/01/2000 12:00:00", "1 12 28 2 *", "28/02/2000 12:01:00", false)] + [TestCase("28/02/2000 12:01:00", "1 12 28 2 *", "28/02/2001 12:01:00", false)] + [TestCase("28/02/2001 12:01:00", "1 12 28 2 *", "28/02/2002 12:01:00", false)] + [TestCase("28/02/2002 12:01:00", "1 12 28 2 *", "28/02/2003 12:01:00", false)] + [TestCase("28/02/2003 12:01:00", "1 12 28 2 *", "28/02/2004 12:01:00", false)] + [TestCase("29/02/2004 12:01:00", "1 12 28 2 *", "28/02/2005 12:01:00", false)] + + [TestCase("01/01/2000 12:00:00", "40 14/1 * * *", "01/01/2000 14:40:00", false)] + [TestCase("01/01/2000 14:40:00", "40 14/1 * * *", "01/01/2000 15:40:00", false)] + + public void Evaluations(string startTimeString, string cronExpression, string nextTimeString, bool includingSeconds) + { + CronCall(startTimeString, cronExpression, nextTimeString, new ParseOptions { - CronFinite(expression, "01/01/2001 00:00:00", "01/01/2010 00:00:00", new ParseOptions - { - IncludingSeconds = includingSeconds - }); - } + IncludingSeconds = includingSeconds + }); + } - static void BadField(string expression, bool includingSeconds) + [TestCase(" * * * * * ", "01/01/2003 00:00:00", "01/01/2003 00:00:00" , false)] + [TestCase(" * * * * * ", "31/12/2002 23:59:59", "01/01/2003 00:00:00" , false)] + [TestCase(" * * * * Mon", "31/12/2002 23:59:59", "01/01/2003 00:00:00" , false)] + [TestCase(" * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 00:00:00" , false)] + [TestCase(" * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 12:00:00" , false)] + [TestCase("30 12 * * Mon", "01/01/2003 00:00:00", "06/01/2003 12:00:00" , false)] + + [TestCase(" * * * * * * ", "01/01/2003 00:00:00", "01/01/2003 00:00:00", true )] + [TestCase(" * * * * * * ", "31/12/2002 23:59:59", "01/01/2003 00:00:00", true )] + [TestCase(" * * * * * Mon", "31/12/2002 23:59:59", "01/01/2003 00:00:00", true )] + [TestCase(" * * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 00:00:00", true )] + [TestCase(" * * * * * Mon", "01/01/2003 00:00:00", "02/01/2003 12:00:00", true )] + [TestCase("10 30 12 * * Mon", "01/01/2003 00:00:00", "06/01/2003 12:00:10", true )] + + public void FiniteOccurrences(string cronExpression, string startTimeString, string endTimeString, bool includingSeconds) + { + CronFinite(cronExpression, startTimeString, endTimeString, new ParseOptions { - Assert.Throws(() => CrontabSchedule.Parse(expression, new ParseOptions - { - IncludingSeconds = includingSeconds - })); - Assert.That(CrontabSchedule.TryParse(expression, new ParseOptions - { - IncludingSeconds = includingSeconds - }), Is.Null); - } - - [TestCase("bad * * * * *", false)] - public void BadSecondsField(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("bad * * * *", false)] - [TestCase("* bad * * * *", true)] - public void BadMinutesField(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("* bad * * *", false)] - [TestCase("* * bad * * *", true)] - public void BadHoursField(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("* * bad * *", false)] - [TestCase("* * * bad * *", true)] - public void BadDayField(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("* * * bad *", false)] - [TestCase("* * * * bad *", true)] - public void BadMonthField(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("* * * * mon,bad,wed", false)] - [TestCase("* * * * * mon,bad,wed", true)] - public void BadDayOfWeekField(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("* 1,2,3,456,7,8,9 * * *", false)] - [TestCase("* * 1,2,3,456,7,8,9 * * *", true)] - public void OutOfRangeField(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("* 1,Z,3,4 * * *", false)] - [TestCase("* * 1,Z,3,4 * * *", true)] - public void NonNumberValueInNumericOnlyField(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("* 1/Z * * *", false)] - [TestCase("* * 1/Z * * *", true)] - public void NonNumericFieldInterval(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - [TestCase("* 3-l2 * * *", false)] - [TestCase("* * 3-l2 * * *", true)] - public void NonNumericFieldRangeComponent(string expression, bool includingSeconds) => - BadField(expression, includingSeconds); - - /// - /// Test case for - /// issue - /// #21 (GetNextOccurrence throws if next occurrence produces - /// invalid time). - /// - - [Test] - public void GetNextOccurrences_NextOccurrenceInvalidTime_ShouldStopAtLastValidTime() + IncludingSeconds = includingSeconds + }); + } + + // + // Test to check we don't loop indefinitely looking for a February + // 31st because no such date would ever exist! + // + + [Category("Performance")] + [Timeout(1000)] + [TestCase("* * 31 Feb *", false)] + [TestCase("* * * 31 Feb *", true)] + public void DontLoopIndefinitely(string expression, bool includingSeconds) + { + CronFinite(expression, "01/01/2001 00:00:00", "01/01/2010 00:00:00", new ParseOptions { - var schedule = CrontabSchedule.Parse("0 0 29 Feb Mon"); - var occurrences = schedule.GetNextOccurrences(new DateTime(9988, 1, 1), DateTime.MaxValue); - Assert.AreEqual(new DateTime(9988, 2, 29), occurrences.Last()); - } - - // Instead of using strings and parsing as date, - // consider NUnit's TestCaseData: - // https://github.com/nunit/docs/wiki/TestCaseData - - [TestCase("0 0 29 Feb Mon", "2017-01-01", "2017-12-31", "2017-12-31")] - [TestCase("0 0 29 Feb Mon", "9000-01-01", "9008-12-31", "9008-02-29")] - public void GetNextOccurence(string expression, string startDate, string endDate, string expectedValue) + IncludingSeconds = includingSeconds + }); + } + + static void BadField(string expression, bool includingSeconds) + { + var options = new ParseOptions { - var schedule = CrontabSchedule.Parse(expression); - var start = Time(startDate); - var end = Time(endDate); - var expected = Time(expectedValue); + IncludingSeconds = includingSeconds + }; + Assert.That(() => CrontabSchedule.Parse(expression, options), + Throws.TypeOf()); + Assert.That(CrontabSchedule.TryParse(expression, options), Is.Null); + } - var occurrence = schedule.GetNextOccurrence(start, end); + [TestCase("bad * * * * *", false)] + public void BadSecondsField(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("bad * * * *", false)] + [TestCase("* bad * * * *", true)] + public void BadMinutesField(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("* bad * * *", false)] + [TestCase("* * bad * * *", true)] + public void BadHoursField(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("* * bad * *", false)] + [TestCase("* * * bad * *", true)] + public void BadDayField(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("* * * bad *", false)] + [TestCase("* * * * bad *", true)] + public void BadMonthField(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("* * * * mon,bad,wed", false)] + [TestCase("* * * * * mon,bad,wed", true)] + public void BadDayOfWeekField(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("* 1,2,3,456,7,8,9 * * *", false)] + [TestCase("* * 1,2,3,456,7,8,9 * * *", true)] + public void OutOfRangeField(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("* 1,Z,3,4 * * *", false)] + [TestCase("* * 1,Z,3,4 * * *", true)] + public void NonNumberValueInNumericOnlyField(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("* 1/Z * * *", false)] + [TestCase("* * 1/Z * * *", true)] + public void NonNumericFieldInterval(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + [TestCase("* 3-l2 * * *", false)] + [TestCase("* * 3-l2 * * *", true)] + public void NonNumericFieldRangeComponent(string expression, bool includingSeconds) => + BadField(expression, includingSeconds); + + /// + /// Test case for + /// issue + /// #21 (GetNextOccurrence throws if next occurrence produces + /// invalid time). + /// + + [Test] + public void GetNextOccurrences_NextOccurrenceInvalidTime_ShouldStopAtLastValidTime() + { + var schedule = CrontabSchedule.Parse("0 0 29 Feb Mon"); + var occurrences = schedule.GetNextOccurrences(new DateTime(9988, 1, 1), DateTime.MaxValue); + Assert.That(occurrences.Last(), Is.EqualTo(new DateTime(9988, 2, 29))); + } - Assert.AreEqual(expected, occurrence); - } + // Instead of using strings and parsing as date, + // consider NUnit's TestCaseData: + // https://github.com/nunit/docs/wiki/TestCaseData + [TestCase("0 0 29 Feb Mon", "2017-01-01", "2017-12-31", "2017-12-31")] + [TestCase("0 0 29 Feb Mon", "9000-01-01", "9008-12-31", "9008-02-29")] + public void GetNextOccurrence(string expression, string startDate, string endDate, string expectedValue) + { + var schedule = CrontabSchedule.Parse(expression); + var start = Time(startDate); + var end = Time(endDate); + var expected = Time(expectedValue); - static void CronCall(string startTimeString, string cronExpression, string nextTimeString, ParseOptions options) - { - var schedule = CrontabSchedule.Parse(cronExpression, options); - var next = schedule.GetNextOccurrence(Time(startTimeString)); + var occurrence = schedule.GetNextOccurrence(start, end); - Assert.AreEqual(nextTimeString, TimeString(next), - "Occurrence of <{0}> after <{1}>.", cronExpression, startTimeString); - } + Assert.That(occurrence, Is.EqualTo(expected)); + } - static void CronFinite(string cronExpression, string startTimeString, string endTimeString, ParseOptions options) - { - var schedule = CrontabSchedule.Parse(cronExpression, options); - var occurrence = schedule.GetNextOccurrence(Time(startTimeString), Time(endTimeString)); + [Test] + public void GetNextOccurrencesWithNullSchedule() + { + // Overload 1 - Assert.AreEqual(endTimeString, TimeString(occurrence), - "Occurrence of <{0}> after <{1}> did not terminate with <{2}>.", - cronExpression, startTimeString, endTimeString); - } + Assert.That(() => CrontabScheduleExtensions.GetNextOccurrences(null!, DateTime.MinValue, DateTime.MaxValue), + Throws.ArgumentNullException + .With.Property(nameof(ArgumentNullException.ParamName)).EqualTo("schedules")); - static string TimeString(DateTime time) => time.ToString(TimeFormat, CultureInfo.InvariantCulture); - static DateTime Time(string str) => DateTime.ParseExact(str, TimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None); + // Overload 2 + + Assert.That(() => CrontabScheduleExtensions.GetNextOccurrences(null!, DateTime.MinValue, DateTime.MaxValue, + delegate { throw new NotImplementedException(); }), + Throws.ArgumentNullException + .With.Property(nameof(ArgumentNullException.ParamName)).EqualTo("schedules")); + } + + [Test] + public void GetNextOccurrencesWithNullResultSelector() + { + Assert.That(() => _ = Enumerable.Empty() + .GetNextOccurrences(DateTime.MinValue, DateTime.MaxValue, resultSelector: null!), + Throws.ArgumentNullException + .With.Property(nameof(ArgumentNullException.ParamName)).EqualTo("resultSelector")); + } + + [TestCase(new[] { "0 * * * *" }, null, + new[] { "01/01/2003 01:00:00", "01/01/2003 02:00:00", "01/01/2003 03:00:00" })] + [TestCase(new[] { "0 1 * * *", " 0 2 * * *", " 0 3 * * *" }, null, + new[] { "01/01/2003 01:00:00", "01/01/2003 02:00:00", "01/01/2003 03:00:00", + "02/01/2003 01:00:00", "02/01/2003 02:00:00", "02/01/2003 03:00:00" })] + [TestCase(new[] { "0 2 * * *", "0 3 * * *", "0 1 * * *" }, null, + new[] { "01/01/2003 01:00:00", "01/01/2003 02:00:00", "01/01/2003 03:00:00", + "02/01/2003 01:00:00", "02/01/2003 02:00:00", "02/01/2003 03:00:00" })] + [TestCase(new[] { "0 * * * *", "0 * * * *" }, "01/01/2003 03:30:00", + new[] { "01/01/2003 01:00:00", "01/01/2003 02:00:00", "01/01/2003 03:00:00" })] + [TestCase(new[] { "0 7-9 * * Mon", "0 6,18 * * Tue", "0 3,*/6 * * Fri" }, null, + new[] { "03/01/2003 00:00:00", "03/01/2003 03:00:00", "03/01/2003 06:00:00", "03/01/2003 12:00:00", "03/01/2003 18:00:00", + "06/01/2003 07:00:00", "06/01/2003 08:00:00", "06/01/2003 09:00:00", + "07/01/2003 06:00:00", "07/01/2003 18:00:00" })] + public void NextOccurrencesFromMultipleSchedules(string[] expressions, string? endTimeString, string[] times) + { + var occurrences = + expressions + .Select(CrontabSchedule.Parse) + .GetNextOccurrences(new DateTime(2003, 1, 1), + endTimeString is { Length: > 0 } someEndTimeString + ? Time(someEndTimeString) + : DateTime.MaxValue) + .Select(TimeString); + + Assert.That(endTimeString is null ? occurrences.Take(times.Length) + : occurrences, + Is.EquivalentTo(times)); + } + + static void CronCall(string startTimeString, string cronExpression, string nextTimeString, ParseOptions options) + { + var schedule = CrontabSchedule.Parse(cronExpression, options); + var next = schedule.GetNextOccurrence(Time(startTimeString)); + + Assert.That(TimeString(next), Is.EqualTo(nextTimeString), + "Occurrence of <{0}> after <{1}>.", cronExpression, startTimeString); + } + + static void CronFinite(string cronExpression, string startTimeString, string endTimeString, ParseOptions options) + { + var schedule = CrontabSchedule.Parse(cronExpression, options); + var occurrence = schedule.GetNextOccurrence(Time(startTimeString), Time(endTimeString)); + + Assert.That(TimeString(occurrence), Is.EqualTo(endTimeString), + "Occurrence of <{0}> after <{1}> did not terminate with <{2}>.", + cronExpression, startTimeString, endTimeString); } -} \ No newline at end of file + + static string TimeString(DateTime time) => time.ToString(TimeFormat, CultureInfo.InvariantCulture); + static DateTime Time(string str) => DateTime.ParseExact(str, TimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None); +} diff --git a/NCrontab.Tests/NCrontab.Tests.csproj b/NCrontab.Tests/NCrontab.Tests.csproj index 4d5722b..74916b3 100644 --- a/NCrontab.Tests/NCrontab.Tests.csproj +++ b/NCrontab.Tests/NCrontab.Tests.csproj @@ -1,11 +1,8 @@ - + - netcoreapp1.0;net451 - portable - true - $(PackageTargetFallback);dotnet5.4;portable-net451+win8 - 1.0.4 + net8.0;net6.0;net451 + false @@ -13,9 +10,13 @@ - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/NCrontab.sln b/NCrontab.sln index 413c8ad..59fbfa3 100644 --- a/NCrontab.sln +++ b/NCrontab.sln @@ -1,14 +1,16 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33723.286 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A4109292-0FE5-4761-868A-31A5BEB1810A}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig appveyor.yml = appveyor.yml build.cmd = build.cmd build.sh = build.sh COPYING.txt = COPYING.txt + Directory.Build.props = Directory.Build.props global.json = global.json pack.cmd = pack.cmd README.md = README.md @@ -17,8 +19,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution test.sh = test.sh EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NCrontabViewer", "NCrontabViewer\NCrontabViewer.csproj", "{02F42DAC-8A9F-45BB-B734-BB08F0D194C4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCrontab", "NCrontab\NCrontab.csproj", "{1B6D7AA9-38DE-45B3-8E90-7FC08E83173F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCrontab.Tests", "NCrontab.Tests\NCrontab.Tests.csproj", "{C0786FAF-E9C1-411C-B70D-5B3339973E57}" @@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCrontabConsole", "NCrontab EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCrontab.Signed", "NCrontab.Signed\NCrontab.Signed.csproj", "{00415F52-1635-4DC7-AA50-4750DC928F89}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCrontabViewer", "NCrontabViewer\NCrontabViewer.csproj", "{DB5C3020-025A-456F-A691-CB7E375A348C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,14 +37,6 @@ Global ReleaseSigned|Any CPU = ReleaseSigned|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4}.DebugSigned|Any CPU.ActiveCfg = Debug|Any CPU - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4}.DebugSigned|Any CPU.Build.0 = Debug|Any CPU - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4}.Release|Any CPU.Build.0 = Release|Any CPU - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4}.ReleaseSigned|Any CPU.ActiveCfg = Release|Any CPU - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4}.ReleaseSigned|Any CPU.Build.0 = Release|Any CPU {1B6D7AA9-38DE-45B3-8E90-7FC08E83173F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1B6D7AA9-38DE-45B3-8E90-7FC08E83173F}.Debug|Any CPU.Build.0 = Debug|Any CPU {1B6D7AA9-38DE-45B3-8E90-7FC08E83173F}.DebugSigned|Any CPU.ActiveCfg = Debug|Any CPU @@ -75,6 +69,14 @@ Global {00415F52-1635-4DC7-AA50-4750DC928F89}.Release|Any CPU.Build.0 = Release|Any CPU {00415F52-1635-4DC7-AA50-4750DC928F89}.ReleaseSigned|Any CPU.ActiveCfg = Release|Any CPU {00415F52-1635-4DC7-AA50-4750DC928F89}.ReleaseSigned|Any CPU.Build.0 = Release|Any CPU + {DB5C3020-025A-456F-A691-CB7E375A348C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB5C3020-025A-456F-A691-CB7E375A348C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB5C3020-025A-456F-A691-CB7E375A348C}.DebugSigned|Any CPU.ActiveCfg = Debug|Any CPU + {DB5C3020-025A-456F-A691-CB7E375A348C}.DebugSigned|Any CPU.Build.0 = Debug|Any CPU + {DB5C3020-025A-456F-A691-CB7E375A348C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB5C3020-025A-456F-A691-CB7E375A348C}.Release|Any CPU.Build.0 = Release|Any CPU + {DB5C3020-025A-456F-A691-CB7E375A348C}.ReleaseSigned|Any CPU.ActiveCfg = Debug|Any CPU + {DB5C3020-025A-456F-A691-CB7E375A348C}.ReleaseSigned|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NCrontab/CrontabException.cs b/NCrontab/CrontabException.cs index a755e0d..b9949d5 100644 --- a/NCrontab/CrontabException.cs +++ b/NCrontab/CrontabException.cs @@ -18,21 +18,20 @@ // #endregion -namespace NCrontab -{ - using System; +using System; + +namespace NCrontab; - // ReSharper disable once PartialTypeWithSinglePart +// ReSharper disable once PartialTypeWithSinglePart - public partial class CrontabException : Exception - { - public CrontabException() : - base("Crontab error.") {} // TODO: Fix message and add it to resource. +public partial class CrontabException : Exception +{ + public CrontabException() : + base("Crontab error.") { } // TODO: Fix message and add it to resource. - public CrontabException(string message) : - base(message) {} + public CrontabException(string? message) : + base(message) { } - public CrontabException(string message, Exception innerException) : - base(message, innerException) {} - } -} \ No newline at end of file + public CrontabException(string? message, Exception? innerException) : + base(message, innerException) { } +} diff --git a/NCrontab/CrontabField.cs b/NCrontab/CrontabField.cs index 71eeebd..7a6900e 100644 --- a/NCrontab/CrontabField.cs +++ b/NCrontab/CrontabField.cs @@ -18,277 +18,270 @@ // #endregion -namespace NCrontab -{ - #region Imports +using System; +using System.Collections; +using System.Globalization; +using System.IO; - using System; - using System.Collections; - using System.Globalization; - using System.IO; +namespace NCrontab; - #endregion +/// +/// Represents a single crontab field. +/// - /// - /// Represents a single crontab field. - /// +// ReSharper disable once PartialTypeWithSinglePart - // ReSharper disable once PartialTypeWithSinglePart +public sealed partial class CrontabField : ICrontabField +{ + readonly BitArray bits; + /* readonly */ int minValueSet; + /* readonly */ int maxValueSet; + readonly CrontabFieldImpl impl; - public sealed partial class CrontabField : ICrontabField - { - readonly BitArray _bits; - /* readonly */ int _minValueSet; - /* readonly */ int _maxValueSet; - readonly CrontabFieldImpl _impl; + /// + /// Parses a crontab field expression given its kind. + /// - /// - /// Parses a crontab field expression given its kind. - /// + public static CrontabField Parse(CrontabFieldKind kind, string expression) => + TryParse(kind, expression, v => v, static e => throw e()); - public static CrontabField Parse(CrontabFieldKind kind, string expression) => - TryParse(kind, expression, v => v, e => { throw e(); }); + public static CrontabField? TryParse(CrontabFieldKind kind, string expression) => + TryParse(kind, expression, static v => (CrontabField?)v, _ => null); - public static CrontabField TryParse(CrontabFieldKind kind, string expression) => - TryParse(kind, expression, v => v, _ => null); + public static T TryParse(CrontabFieldKind kind, string expression, + Func valueSelector, + Func errorSelector) + { + if (valueSelector == null) throw new ArgumentNullException(nameof(valueSelector)); + if (errorSelector == null) throw new ArgumentNullException(nameof(errorSelector)); - public static T TryParse(CrontabFieldKind kind, string expression, Func valueSelector, Func errorSelector) - { - var field = new CrontabField(CrontabFieldImpl.FromKind(kind)); - var error = field._impl.TryParse(expression, field.Accumulate, null, e => e); - return error == null ? valueSelector(field) : errorSelector(error); - } + var field = new CrontabField(CrontabFieldImpl.FromKind(kind)); + var error = field.impl.TryParse(expression, field.Accumulate, null, static e => e); + return error == null ? valueSelector(field) : errorSelector(error); + } - /// - /// Parses a crontab field expression representing seconds. - /// + /// + /// Parses a crontab field expression representing seconds. + /// - public static CrontabField Seconds(string expression) => - Parse(CrontabFieldKind.Second, expression); + public static CrontabField Seconds(string expression) => + Parse(CrontabFieldKind.Second, expression); - /// - /// Parses a crontab field expression representing minutes. - /// + /// + /// Parses a crontab field expression representing minutes. + /// - public static CrontabField Minutes(string expression) => - Parse(CrontabFieldKind.Minute, expression); + public static CrontabField Minutes(string expression) => + Parse(CrontabFieldKind.Minute, expression); - /// - /// Parses a crontab field expression representing hours. - /// + /// + /// Parses a crontab field expression representing hours. + /// - public static CrontabField Hours(string expression) => - Parse(CrontabFieldKind.Hour, expression); + public static CrontabField Hours(string expression) => + Parse(CrontabFieldKind.Hour, expression); - /// - /// Parses a crontab field expression representing days in any given month. - /// + /// + /// Parses a crontab field expression representing days in any given month. + /// - public static CrontabField Days(string expression) => - Parse(CrontabFieldKind.Day, expression); + public static CrontabField Days(string expression) => + Parse(CrontabFieldKind.Day, expression); - /// - /// Parses a crontab field expression representing months. - /// + /// + /// Parses a crontab field expression representing months. + /// - public static CrontabField Months(string expression) => - Parse(CrontabFieldKind.Month, expression); + public static CrontabField Months(string expression) => + Parse(CrontabFieldKind.Month, expression); - /// - /// Parses a crontab field expression representing days of a week. - /// + /// + /// Parses a crontab field expression representing days of a week. + /// - public static CrontabField DaysOfWeek(string expression) => - Parse(CrontabFieldKind.DayOfWeek, expression); + public static CrontabField DaysOfWeek(string expression) => + Parse(CrontabFieldKind.DayOfWeek, expression); - CrontabField(CrontabFieldImpl impl) - { - if (impl == null) throw new ArgumentNullException(nameof(impl)); + CrontabField(CrontabFieldImpl impl) + { + this.impl = impl ?? throw new ArgumentNullException(nameof(impl)); + this.bits = new BitArray(impl.ValueCount); + this.minValueSet = int.MaxValue; + this.maxValueSet = -1; + } - _impl = impl; - _bits = new BitArray(impl.ValueCount); + /// + /// Gets the first value of the field or -1. + /// - _bits.SetAll(false); - _minValueSet = int.MaxValue; - _maxValueSet = -1; - } + public int GetFirst() => this.minValueSet < int.MaxValue ? this.minValueSet : -1; - /// - /// Gets the first value of the field or -1. - /// + /// + /// Gets the next value of the field that occurs after the given start value or -1 if there is + /// no next value available. + /// - public int GetFirst() => _minValueSet < int.MaxValue ? _minValueSet : -1; + public int Next(int start) + { + if (start < this.minValueSet) + return this.minValueSet; - /// - /// Gets the next value of the field that occurs after the given - /// start value or -1 if there is no next value available. - /// + var startIndex = ValueToIndex(start); + var lastIndex = ValueToIndex(this.maxValueSet); - public int Next(int start) + for (var i = startIndex; i <= lastIndex; i++) { - if (start < _minValueSet) - return _minValueSet; - - var startIndex = ValueToIndex(start); - var lastIndex = ValueToIndex(_maxValueSet); + if (this.bits[i]) + return IndexToValue(i); + } - for (var i = startIndex; i <= lastIndex; i++) - { - if (_bits[i]) - return IndexToValue(i); - } + return -1; + } - return -1; - } + int IndexToValue(int index) => index + this.impl.MinValue; + int ValueToIndex(int value) => value - this.impl.MinValue; - int IndexToValue(int index) => index + _impl.MinValue; - int ValueToIndex(int value) => value - _impl.MinValue; + /// + /// Determines if the given value occurs in the field. + /// - /// - /// Determines if the given value occurs in the field. - /// + public bool Contains(int value) => this.bits[ValueToIndex(value)]; - public bool Contains(int value) => _bits[ValueToIndex(value)]; + /// + /// Accumulates the given range (start to end) and interval of values into the current set of + /// the field. + /// + /// + /// To set the entire range of values representable by the field, set and + /// to -1 and to 1. + /// - /// - /// Accumulates the given range (start to end) and interval of values - /// into the current set of the field. - /// - /// - /// To set the entire range of values representable by the field, - /// set and to -1 and - /// to 1. - /// + T Accumulate(int start, int end, int interval, T success, Func errorSelector) + { + var minValue = this.impl.MinValue; + var maxValue = this.impl.MaxValue; - T Accumulate(int start, int end, int interval, T success, Func errorSelector) + if (start == end) { - var minValue = _impl.MinValue; - var maxValue = _impl.MaxValue; - - if (start == end) + if (start < 0) { - if (start < 0) + // + // We're setting the entire range of values. + // + + if (interval <= 1) { - // - // We're setting the entire range of values. - // - - if (interval <= 1) - { - _minValueSet = minValue; - _maxValueSet = maxValue; - _bits.SetAll(true); - return success; - } - - start = minValue; - end = maxValue; + this.minValueSet = minValue; + this.maxValueSet = maxValue; + this.bits.SetAll(true); + return success; } - else - { - // - // We're only setting a single value - check that it is in range. - // - if (start < minValue) - return OnValueBelowMinError(start, errorSelector); - - if (start > maxValue) - return OnValueAboveMaxError(start, errorSelector); - } + start = minValue; + end = maxValue; } else { // - // For ranges, if the start is bigger than the end value then - // swap them over. + // We're only setting a single value - check that it is in range. // - if (start > end) - { - end ^= start; - start ^= end; - end ^= start; - } + if (start < minValue) + return OnValueBelowMinError(start); - if (start < 0) - start = minValue; - else if (start < minValue) - return OnValueBelowMinError(start, errorSelector); + if (start > maxValue) + return OnValueAboveMaxError(start); + } + } + else + { + // + // For ranges, if the start is bigger than the end value then + // swap them over. + // - if (end < 0) - end = maxValue; - else if (end > maxValue) - return OnValueAboveMaxError(end, errorSelector); + if (start > end) + { + end ^= start; + start ^= end; + end ^= start; } - if (interval < 1) - interval = 1; + if (start < 0) + start = minValue; + else if (start < minValue) + return OnValueBelowMinError(start); - int i; + if (end < 0) + end = maxValue; + else if (end > maxValue) + return OnValueAboveMaxError(end); + } - // - // Populate the _bits table by setting all the bits corresponding to - // the valid field values. - // + if (interval < 1) + interval = 1; - for (i = start - minValue; i <= (end - minValue); i += interval) - _bits[i] = true; + int i; - // - // Make sure we remember the minimum value set so far Keep track of - // the highest and lowest values that have been added to this field - // so far. - // + // + // Populate the _bits table by setting all the bits corresponding to the valid field values. + // - if (_minValueSet > start) - _minValueSet = start; + for (i = start - minValue; i <= (end - minValue); i += interval) + this.bits[i] = true; - i += (minValue - interval); + // + // Make sure we remember the minimum value set so far Keep track of the highest and lowest + // values that have been added to this field so far. + // - if (_maxValueSet < i) - _maxValueSet = i; + if (this.minValueSet > start) + this.minValueSet = start; - return success; - } + i += minValue - interval; + + if (this.maxValueSet < i) + this.maxValueSet = i; - T OnValueAboveMaxError(int value, Func errorSelector) => + return success; + + T OnValueAboveMaxError(int value) => errorSelector( () => new CrontabException( - $"{value} is higher than the maximum allowable value for the [{_impl.Kind}] field. " + - $"Value must be between {_impl.MinValue} and {_impl.MaxValue} (all inclusive).")); + $"{value} is higher than the maximum allowable value for the [{this.impl.Kind}] field. " + + $"Value must be between {this.impl.MinValue} and {this.impl.MaxValue} (all inclusive).")); - T OnValueBelowMinError(int value, Func errorSelector) => + T OnValueBelowMinError(int value) => errorSelector( () => new CrontabException( - $"{value} is lower than the minimum allowable value for the [{_impl.Kind}] field. " + - $"Value must be between {_impl.MinValue} and {_impl.MaxValue} (all inclusive).")); - - public override string ToString() => ToString(null); + $"{value} is lower than the minimum allowable value for the [{this.impl.Kind}] field. " + + $"Value must be between {this.impl.MinValue} and {this.impl.MaxValue} (all inclusive).")); + } - public string ToString(string format) - { - var writer = new StringWriter(CultureInfo.InvariantCulture); + public override string ToString() => ToString(null); - switch (format) - { - case "G": - case null: - Format(writer, true); - break; - case "N": - Format(writer); - break; - default: - throw new FormatException(); - } + public string ToString(string? format) + { + using var writer = new StringWriter(CultureInfo.InvariantCulture); - return writer.ToString(); + switch (format) + { + case "G": + case null: + Format(writer, true); + break; + case "N": + Format(writer); + break; + default: + throw new FormatException(); } - public void Format(TextWriter writer) => Format(writer, false); - - public void Format(TextWriter writer, bool noNames) => - _impl.Format(this, writer, noNames); + return writer.ToString(); } + + public void Format(TextWriter writer) => Format(writer, false); + + public void Format(TextWriter writer, bool noNames) => + this.impl.Format(this, writer, noNames); } diff --git a/NCrontab/CrontabFieldImpl.cs b/NCrontab/CrontabFieldImpl.cs index 20df154..65cdd47 100644 --- a/NCrontab/CrontabFieldImpl.cs +++ b/NCrontab/CrontabFieldImpl.cs @@ -18,288 +18,250 @@ // #endregion -namespace NCrontab -{ - #region Imports - - using System; - using System.Collections.Generic; - using System.Globalization; - using System.IO; - using Debug = System.Diagnostics.Debug; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using Debug = System.Diagnostics.Debug; - #endregion +namespace NCrontab; - delegate T CrontabFieldAccumulator(int start, int end, int interval, T successs, Func onError); +delegate T CrontabFieldAccumulator(int start, int end, int interval, T success, Func onError); - // ReSharper disable once PartialTypeWithSinglePart +// ReSharper disable once PartialTypeWithSinglePart - sealed partial class CrontabFieldImpl - { - public static readonly CrontabFieldImpl Second = new CrontabFieldImpl(CrontabFieldKind.Second, 0, 59, null); - public static readonly CrontabFieldImpl Minute = new CrontabFieldImpl(CrontabFieldKind.Minute, 0, 59, null); - public static readonly CrontabFieldImpl Hour = new CrontabFieldImpl(CrontabFieldKind.Hour, 0, 23, null); - public static readonly CrontabFieldImpl Day = new CrontabFieldImpl(CrontabFieldKind.Day, 1, 31, null); - public static readonly CrontabFieldImpl Month = new CrontabFieldImpl(CrontabFieldKind.Month, 1, 12, new[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }); - public static readonly CrontabFieldImpl DayOfWeek = new CrontabFieldImpl(CrontabFieldKind.DayOfWeek, 0, 6, new[] { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }); +sealed partial class CrontabFieldImpl +{ + public static readonly CrontabFieldImpl Second = new(CrontabFieldKind.Second, 0, 59, null); + public static readonly CrontabFieldImpl Minute = new(CrontabFieldKind.Minute, 0, 59, null); + public static readonly CrontabFieldImpl Hour = new(CrontabFieldKind.Hour, 0, 23, null); + public static readonly CrontabFieldImpl Day = new(CrontabFieldKind.Day, 1, 31, null); + public static readonly CrontabFieldImpl Month = new(CrontabFieldKind.Month, 1, 12, ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]); + public static readonly CrontabFieldImpl DayOfWeek = new(CrontabFieldKind.DayOfWeek, 0, 6, ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]); - static readonly CrontabFieldImpl[] FieldByKind = { Second, Minute, Hour, Day, Month, DayOfWeek }; + static readonly CrontabFieldImpl[] FieldByKind = [Second, Minute, Hour, Day, Month, DayOfWeek]; - static readonly CompareInfo Comparer = CultureInfo.InvariantCulture.CompareInfo; + static readonly CompareInfo Comparer = CultureInfo.InvariantCulture.CompareInfo; - readonly string[] _names; + readonly string[]? names; // TODO reconsider empty array == unnamed - public static CrontabFieldImpl FromKind(CrontabFieldKind kind) + public static CrontabFieldImpl FromKind(CrontabFieldKind kind) + { + if (!Enum.IsDefined(typeof(CrontabFieldKind), kind)) { - if (!Enum.IsDefined(typeof(CrontabFieldKind), kind)) - { - var kinds = string.Join(", ", Enum.GetNames(typeof(CrontabFieldKind))); - throw new ArgumentException($"Invalid crontab field kind. Valid values are {kinds}.", nameof(kind)); - } - - return FieldByKind[(int) kind]; + var kinds = string.Join(", ", Enum.GetNames(typeof(CrontabFieldKind))); + throw new ArgumentException($"Invalid crontab field kind. Valid values are {kinds}.", nameof(kind)); } - CrontabFieldImpl(CrontabFieldKind kind, int minValue, int maxValue, string[] names) - { - Debug.Assert(Enum.IsDefined(typeof(CrontabFieldKind), kind)); - Debug.Assert(minValue >= 0); - Debug.Assert(maxValue >= minValue); - Debug.Assert(names == null || names.Length == (maxValue - minValue + 1)); - - Kind = kind; - MinValue = minValue; - MaxValue = maxValue; - _names = names; - } + return FieldByKind[(int)kind]; + } - public CrontabFieldKind Kind { get; } - public int MinValue { get; } - public int MaxValue { get; } + CrontabFieldImpl(CrontabFieldKind kind, int minValue, int maxValue, string[]? names) + { + Debug.Assert(Enum.IsDefined(typeof(CrontabFieldKind), kind)); + Debug.Assert(minValue >= 0); + Debug.Assert(maxValue >= minValue); + Debug.Assert(names == null || names.Length == (maxValue - minValue + 1)); + + Kind = kind; + MinValue = minValue; + MaxValue = maxValue; + this.names = names; + } - public int ValueCount => MaxValue - MinValue + 1; + public CrontabFieldKind Kind { get; } + public int MinValue { get; } + public int MaxValue { get; } - public void Format(ICrontabField field, TextWriter writer) => - Format(field, writer, false); + public int ValueCount => MaxValue - MinValue + 1; - public void Format(ICrontabField field, TextWriter writer, bool noNames) - { - if (field == null) throw new ArgumentNullException(nameof(field)); - if (writer == null) throw new ArgumentNullException(nameof(writer)); + public void Format(ICrontabField field, TextWriter writer) => + Format(field, writer, false); - var next = field.GetFirst(); - var count = 0; + public void Format(ICrontabField field, TextWriter writer, bool noNames) + { + if (field == null) throw new ArgumentNullException(nameof(field)); + if (writer == null) throw new ArgumentNullException(nameof(writer)); - while (next != -1) - { - var first = next; - int last; - - do - { - last = next; - next = field.Next(last + 1); - } - while (next - last == 1); - - if (count == 0 - && first == MinValue && last == MaxValue) - { - writer.Write('*'); - return; - } - - if (count > 0) - writer.Write(','); - - if (first == last) - { - FormatValue(first, writer, noNames); - } - else - { - FormatValue(first, writer, noNames); - writer.Write('-'); - FormatValue(last, writer, noNames); - } - - count++; - } - } + var next = field.GetFirst(); + var count = 0; - void FormatValue(int value, TextWriter writer, bool noNames) + while (next != -1) { - Debug.Assert(writer != null); + var first = next; + int last; - if (noNames || _names == null) + do { - if (value >= 0 && value < 100) - { - FastFormatNumericValue(value, writer); - } - else - { - writer.Write(value.ToString(CultureInfo.InvariantCulture)); - } + last = next; + next = field.Next(last + 1); } - else + while (next - last == 1); + + if (count == 0 + && first == MinValue && last == MaxValue) { - var index = value - MinValue; - writer.Write(_names[index]); + writer.Write('*'); + return; } - } - static void FastFormatNumericValue(int value, TextWriter writer) - { - Debug.Assert(value >= 0 && value < 100); - Debug.Assert(writer != null); + if (count > 0) + writer.Write(','); - if (value >= 10) + if (first == last) { - writer.Write((char) ('0' + (value / 10))); - writer.Write((char) ('0' + (value % 10))); + FormatValue(first, writer, noNames); } else { - writer.Write((char) ('0' + value)); + FormatValue(first, writer, noNames); + writer.Write('-'); + FormatValue(last, writer, noNames); } - } - public void Parse(string str, CrontabFieldAccumulator acc) => - TryParse(str, acc, null, ep => { throw ep(); }); + count++; + } + } - public T TryParse(string str, CrontabFieldAccumulator acc, T success, Func errorSelector) + void FormatValue(int value, TextWriter writer, bool noNames) + { + if (noNames || this.names == null) { - if (acc == null) throw new ArgumentNullException(nameof(acc)); - - if (string.IsNullOrEmpty(str)) - return success; - - try - { - return InternalParse(str, acc, success, errorSelector); - } - catch (FormatException e) + if (value is >= 0 and < 100) { - return OnParseException(e, str, errorSelector); + FastFormatNumericValue(value, writer); } - catch (CrontabException e) + else { - return OnParseException(e, str, errorSelector); + writer.Write(value.ToString(CultureInfo.InvariantCulture)); } } - - T OnParseException(Exception innerException, string str, Func errorSelector) + else { - Debug.Assert(str != null); - Debug.Assert(innerException != null); - - return errorSelector( - () => new CrontabException($"'{str}' is not a valid [{Kind}] crontab field expression.", innerException)); + var index = value - MinValue; + writer.Write(this.names[index]); } + } - T InternalParse(string str, CrontabFieldAccumulator acc, T success, Func errorSelector) + static void FastFormatNumericValue(int value, TextWriter writer) + { + Debug.Assert(value is >= 0 and < 100); + + if (value >= 10) { - Debug.Assert(str != null); - Debug.Assert(acc != null); + writer.Write((char)('0' + (value / 10))); + writer.Write((char)('0' + (value % 10))); + } + else + { + writer.Write((char)('0' + value)); + } + } - if (str.Length == 0) - return errorSelector(() => new CrontabException("A crontab field value cannot be empty.")); + public void Parse(string str, CrontabFieldAccumulator acc) => + _ = TryParse(str, acc, null, static ep => throw ep()); - // - // Next, look for a list of values (e.g. 1,2,3). - // + public T TryParse(string str, CrontabFieldAccumulator acc, T success, + Func errorSelector) + { + if (acc == null) throw new ArgumentNullException(nameof(acc)); - var commaIndex = str.IndexOf(','); + if (string.IsNullOrEmpty(str)) + return success; - if (commaIndex > 0) - { - var result = success; - using (var token = ((IEnumerable) str.Split(StringSeparatorStock.Comma)).GetEnumerator()) - { - while (token.MoveNext() && result == null) - result = InternalParse(token.Current, acc, success, errorSelector); - } - return result; - } + try + { + return InternalParse(str, acc, success, errorSelector); + } + catch (Exception e) when (e is FormatException or CrontabException) + { + return errorSelector(() => new CrontabException($"'{str}' is not a valid [{Kind}] crontab field expression.", e)); + } + } - int? every = null; + T InternalParse(string str, CrontabFieldAccumulator acc, T success, Func errorSelector) + { + if (str.Length == 0) + return errorSelector(() => new CrontabException("A crontab field value cannot be empty.")); - // - // Look for stepping first (e.g. */2 = every 2nd). - // + // + // Next, look for a list of values (e.g. 1,2,3). + // - var slashIndex = str.IndexOf('/'); + if (str.IndexOf(',') > 0) + { + var result = success; + using var token = ((IEnumerable)str.Split(StringSeparatorStock.Comma)).GetEnumerator(); + while (token.MoveNext() && result == null) + result = InternalParse(token.Current, acc, success, errorSelector); + return result; + } - if (slashIndex > 0) - { - every = int.Parse(str.Substring(slashIndex + 1), CultureInfo.InvariantCulture); - str = str.Substring(0, slashIndex); - } + int? every = null; - // - // Next, look for wildcard (*). - // + // + // Look for stepping first (e.g. */2 = every 2nd). + // - if (str.Length == 1 && str[0]== '*') - { - return acc(-1, -1, every ?? 1, success, errorSelector); - } + if (str.IndexOf('/') is var slashIndex and > 0) + { + every = int.Parse(str.Substring(slashIndex + 1), CultureInfo.InvariantCulture); + str = str.Substring(0, slashIndex); + } - // - // Next, look for a range of values (e.g. 2-10). - // + // + // Next, look for wildcard (*). + // - var dashIndex = str.IndexOf('-'); + if (str.Length == 1 && str[0] == '*') + { + return acc(-1, -1, every ?? 1, success, errorSelector); + } - if (dashIndex > 0) - { - var first = ParseValue(str.Substring(0, dashIndex)); - var last = ParseValue(str.Substring(dashIndex + 1)); + // + // Next, look for a range of values (e.g. 2-10). + // - return acc(first, last, every ?? 1, success, errorSelector); - } + if (str.IndexOf('-') is var dashIndex and > 0) + { + var first = ParseValue(str.Substring(0, dashIndex)); + var last = ParseValue(str.Substring(dashIndex + 1)); - // - // Finally, handle the case where there is only one number. - // + return acc(first, last, every ?? 1, success, errorSelector); + } - var value = ParseValue(str); + // + // Finally, handle the case where there is only one number. + // - if (every == null) - return acc(value, value, 1, success, errorSelector); + var value = ParseValue(str); - Debug.Assert(every != 0); - return acc(value, MaxValue, every.Value, success, errorSelector); - } + return every is { } someEvery + ? acc(value, MaxValue, someEvery, success, errorSelector) + : acc(value, value, 1, success, errorSelector); int ParseValue(string str) { - Debug.Assert(str != null); - if (str.Length == 0) throw new CrontabException("A crontab field value cannot be empty."); - var firstChar = str[0]; - - if (firstChar >= '0' && firstChar <= '9') + if (str[0] is >= '0' and <= '9') return int.Parse(str, CultureInfo.InvariantCulture); - if (_names == null) + if (this.names == null) { - throw new CrontabException(string.Format( - "'{0}' is not a valid [{3}] crontab field value. It must be a numeric value between {1} and {2} (all inclusive).", - str, MinValue.ToString(), MaxValue.ToString(), Kind.ToString())); + throw new CrontabException($"'{str}' is not a valid [{Kind}] crontab field value. It must be a numeric value between {MinValue} and {MaxValue} (all inclusive)."); } - for (var i = 0; i < _names.Length; i++) + for (var i = 0; i < this.names.Length; i++) { - if (Comparer.IsPrefix(_names[i], str, CompareOptions.IgnoreCase)) + if (Comparer.IsPrefix(this.names[i], str, CompareOptions.IgnoreCase)) return i + MinValue; } - var names = string.Join(", ", _names); + var names = string.Join(", ", this.names); throw new CrontabException($"'{str}' is not a known value name. Use one of the following: {names}."); } } + } diff --git a/NCrontab/CrontabFieldKind.cs b/NCrontab/CrontabFieldKind.cs index 4f66d31..60075c6 100644 --- a/NCrontab/CrontabFieldKind.cs +++ b/NCrontab/CrontabFieldKind.cs @@ -18,15 +18,14 @@ // #endregion -namespace NCrontab +namespace NCrontab; + +public enum CrontabFieldKind { - public enum CrontabFieldKind - { - Second = 0, // Keep in order of appearance in expression - Minute = 1, - Hour = 2, - Day = 3, - Month = 4, - DayOfWeek = 5 - } -} \ No newline at end of file + Second = 0, // Keep in order of appearance in expression + Minute = 1, + Hour = 2, + Day = 3, + Month = 4, + DayOfWeek = 5 +} diff --git a/NCrontab/CrontabSchedule.cs b/NCrontab/CrontabSchedule.cs index 28e0045..a409eb4 100644 --- a/NCrontab/CrontabSchedule.cs +++ b/NCrontab/CrontabSchedule.cs @@ -18,384 +18,380 @@ // #endregion -namespace NCrontab -{ - #region Imports +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; - using System; - using System.Collections.Generic; - using System.Globalization; - using System.IO; - using Debug = System.Diagnostics.Debug; +namespace NCrontab; - #endregion +/// +/// Represents a schedule initialized from the crontab expression. +/// - /// - /// Represents a schedule initialized from the crontab expression. - /// +// ReSharper disable once PartialTypeWithSinglePart + +public sealed partial class CrontabSchedule +{ + readonly CrontabField? seconds; + readonly CrontabField minutes; + readonly CrontabField hours; + readonly CrontabField days; + readonly CrontabField months; + readonly CrontabField daysOfWeek; + + static readonly CrontabField SecondZero = CrontabField.Seconds("0"); // ReSharper disable once PartialTypeWithSinglePart - public sealed partial class CrontabSchedule +#pragma warning disable CA1034 // Nested types should not be visible (by design) + public sealed partial class ParseOptions +#pragma warning restore CA1034 // Nested types should not be visible { - readonly CrontabField _seconds; - readonly CrontabField _minutes; - readonly CrontabField _hours; - readonly CrontabField _days; - readonly CrontabField _months; - readonly CrontabField _daysOfWeek; + public bool IncludingSeconds { get; set; } + } - static readonly CrontabField SecondZero = CrontabField.Seconds("0"); + // + // Crontab expression format: + // + // * * * * * + // - - - - - + // | | | | | + // | | | | +----- day of week (0 - 6) (Sunday=0) + // | | | +------- month (1 - 12) + // | | +--------- day of month (1 - 31) + // | +----------- hour (0 - 23) + // +------------- min (0 - 59) + // + // Star (*) in the value field above means all legal values as in braces for that column. The + // value column can have a * or a list of elements separated by commas. An element is either a + // number in the ranges shown above or two numbers in the range separated by a hyphen (meaning + // an inclusive range). + // + // Source: http://www.adminschoice.com/docs/crontab.htm + // + // + // Six-part expression format: + // + // * * * * * * + // - - - - - - + // | | | | | | + // | | | | | +--- day of week (0 - 6) (Sunday=0) + // | | | | +----- month (1 - 12) + // | | | +------- day of month (1 - 31) + // | | +--------- hour (0 - 23) + // | +----------- min (0 - 59) + // +------------- sec (0 - 59) + // + // The six-part expression behaves similarly to the traditional crontab format except that it + // can denotate more precise schedules that use a seconds component. + // + + public static CrontabSchedule Parse(string expression) => Parse(expression, null); + + public static CrontabSchedule Parse(string expression, ParseOptions? options) => + TryParse(expression, options, static v => v, e => throw e()); + + public static CrontabSchedule? TryParse(string expression) => TryParse(expression, null); + + public static CrontabSchedule? TryParse(string expression, ParseOptions? options) => + TryParse(expression ?? string.Empty, options, static v => (CrontabSchedule?)v, _ => null); + + public static T TryParse(string expression, + Func valueSelector, + Func errorSelector) => + TryParse(expression ?? string.Empty, null, valueSelector, errorSelector); + + public static T TryParse(string expression, ParseOptions? options, + Func valueSelector, + Func errorSelector) + { + if (expression == null) throw new ArgumentNullException(nameof(expression)); + if (valueSelector == null) throw new ArgumentNullException(nameof(valueSelector)); + if (errorSelector == null) throw new ArgumentNullException(nameof(errorSelector)); - // ReSharper disable once PartialTypeWithSinglePart + var tokens = expression.Split(StringSeparatorStock.Space, StringSplitOptions.RemoveEmptyEntries); - public sealed partial class ParseOptions + var includingSeconds = options is { IncludingSeconds: true }; + var expectedTokenCount = includingSeconds ? 6 : 5; + if (tokens.Length < expectedTokenCount || tokens.Length > expectedTokenCount) { - public bool IncludingSeconds { get; set; } + return errorSelector(() => + { + var components = + includingSeconds + ? "6 components of a schedule in the sequence of seconds, minutes, hours, days, months, and days of week" + : "5 components of a schedule in the sequence of minutes, hours, days, months, and days of week"; + return new CrontabException($"'{expression}' is an invalid crontab expression. It must contain {components}."); + }); } - // - // Crontab expression format: - // - // * * * * * - // - - - - - - // | | | | | - // | | | | +----- day of week (0 - 6) (Sunday=0) - // | | | +------- month (1 - 12) - // | | +--------- day of month (1 - 31) - // | +----------- hour (0 - 23) - // +------------- min (0 - 59) - // - // Star (*) in the value field above means all legal values as in - // braces for that column. The value column can have a * or a list - // of elements separated by commas. An element is either a number in - // the ranges shown above or two numbers in the range separated by a - // hyphen (meaning an inclusive range). - // - // Source: http://www.adminschoice.com/docs/crontab.htm - // + var fields = new CrontabField[6]; - // Six-part expression format: - // - // * * * * * * - // - - - - - - - // | | | | | | - // | | | | | +--- day of week (0 - 6) (Sunday=0) - // | | | | +----- month (1 - 12) - // | | | +------- day of month (1 - 31) - // | | +--------- hour (0 - 23) - // | +----------- min (0 - 59) - // +------------- sec (0 - 59) - // - // The six-part expression behaves similarly to the traditional - // crontab format except that it can denotate more precise schedules - // that use a seconds component. - // + var offset = includingSeconds ? 0 : 1; + for (var i = 0; i < tokens.Length; i++) + { + var kind = (CrontabFieldKind)i + offset; + var field = CrontabField.TryParse(kind, tokens[i], static v => new { ErrorProvider = (ExceptionProvider?)null, Value = (CrontabField?)v }, + static e => new { ErrorProvider = (ExceptionProvider?)e , Value = (CrontabField?)null }) ; - public static CrontabSchedule Parse(string expression) => Parse(expression, null); + if (field.ErrorProvider != null) + return errorSelector(field.ErrorProvider); + fields[i + offset] = field.Value!; // non-null by mutual exclusivity! + } + + return valueSelector(new CrontabSchedule(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5])); + } - public static CrontabSchedule Parse(string expression, ParseOptions options) => - TryParse(expression, options, v => v, e => { throw e(); }); + CrontabSchedule(CrontabField? seconds, + CrontabField minutes, CrontabField hours, + CrontabField days, CrontabField months, + CrontabField daysOfWeek) + { + this.seconds = seconds; + this.minutes = minutes; + this.hours = hours; + this.days = days; + this.months = months; + this.daysOfWeek = daysOfWeek; + } - public static CrontabSchedule TryParse(string expression) => TryParse(expression, null); + /// + /// Enumerates all the occurrences of this schedule starting with a base time and up to an end + /// time limit. This method uses deferred execution such that the occurrences are only + /// calculated as they are enumerated. + /// + /// + /// This method does not return the value of itself if it falls on + /// the schedule. For example, if is midnight and the schedule was + /// created from the expression * * * * * (meaning every minute) then the next occurrence + /// of the schedule will be at one minute past midnight and not midnight itself. The method + /// returns the next occurrence after . Also, + /// is exclusive. + /// + + public IEnumerable GetNextOccurrences(DateTime baseTime, DateTime endTime) + { + for (var occurrence = TryGetNextOccurrence(baseTime, endTime); + occurrence != null && occurrence < endTime; + occurrence = TryGetNextOccurrence(occurrence.Value, endTime)) + { + yield return occurrence.Value; + } + } - public static CrontabSchedule TryParse(string expression, ParseOptions options) => - TryParse(expression ?? string.Empty, options, v => v, _ => null); + /// + /// Gets the next occurrence of this schedule starting with a base time. + /// - public static T TryParse(string expression, Func valueSelector, Func errorSelector) => - TryParse(expression ?? string.Empty, null, valueSelector, errorSelector); + public DateTime GetNextOccurrence(DateTime baseTime) => + GetNextOccurrence(baseTime, DateTime.MaxValue); - public static T TryParse(string expression, ParseOptions options, Func valueSelector, Func errorSelector) - { - if (expression == null) throw new ArgumentNullException(nameof(expression)); + /// + /// Gets the next occurrence of this schedule starting with a base time and up to an end time + /// limit. + /// + /// + /// This method does not return the value of itself if it falls on + /// the schedule. For example, if is midnight and the schedule was + /// created from the expression * * * * * (meaning every minute) then the next occurrence + /// of the schedule will be at one minute past midnight and not midnight itself. The method + /// returns the next occurrence after . Also, + /// is exclusive. + /// + + public DateTime GetNextOccurrence(DateTime baseTime, DateTime endTime) => + TryGetNextOccurrence(baseTime, endTime) ?? endTime; + + DateTime? TryGetNextOccurrence(DateTime baseTime, DateTime endTime) + { + const int nil = -1; + + var baseYear = baseTime.Year; + var baseMonth = baseTime.Month; + var baseDay = baseTime.Day; + var baseHour = baseTime.Hour; + var baseMinute = baseTime.Minute; + var baseSecond = baseTime.Second; + + var endYear = endTime.Year; + var endMonth = endTime.Month; + var endDay = endTime.Day; + + var year = baseYear; + var month = baseMonth; + var day = baseDay; + var hour = baseHour; + var minute = baseMinute; + var second = baseSecond + 1; - var tokens = expression.Split(StringSeparatorStock.Space, StringSplitOptions.RemoveEmptyEntries); + // + // Second + // - var includingSeconds = options != null && options.IncludingSeconds; - var expectedTokenCount = includingSeconds ? 6 : 5; - if (tokens.Length < expectedTokenCount || tokens.Length > expectedTokenCount) - { - return errorSelector(() => - { - var components = - includingSeconds - ? "6 components of a schedule in the sequence of seconds, minutes, hours, days, months, and days of week" - : "5 components of a schedule in the sequence of minutes, hours, days, months, and days of week"; - return new CrontabException($"'{expression}' is an invalid crontab expression. It must contain {components}."); - }); - } - - var fields = new CrontabField[6]; - - var offset = includingSeconds ? 0 : 1; - for (var i = 0; i < tokens.Length; i++) - { - var kind = (CrontabFieldKind) i + offset; - var field = CrontabField.TryParse(kind, tokens[i], v => new { ErrorProvider = (ExceptionProvider) null, Value = v }, - e => new { ErrorProvider = e, Value = (CrontabField) null }); - if (field.ErrorProvider != null) - return errorSelector(field.ErrorProvider); - fields[i + offset] = field.Value; - } - - return valueSelector(new CrontabSchedule(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5])); - } + var seconds = this.seconds ?? SecondZero; + second = seconds.Next(second); - CrontabSchedule( - CrontabField seconds, - CrontabField minutes, CrontabField hours, - CrontabField days, CrontabField months, - CrontabField daysOfWeek) + if (second == nil) { - Debug.Assert(minutes != null); - Debug.Assert(hours != null); - Debug.Assert(days != null); - Debug.Assert(months != null); - Debug.Assert(daysOfWeek != null); - - _seconds = seconds; - _minutes = minutes; - _hours = hours; - _days = days; - _months = months; - _daysOfWeek = daysOfWeek; + second = seconds.GetFirst(); + minute++; } - /// - /// Enumerates all the occurrences of this schedule starting with a - /// base time and up to an end time limit. This method uses deferred - /// execution such that the occurrences are only calculated as they - /// are enumerated. - /// - /// - /// This method does not return the value of - /// itself if it falls on the schedule. For example, if - /// is midnight and the schedule was created from the expression * * * * * - /// (meaning every minute) then the next occurrence of the schedule - /// will be at one minute past midnight and not midnight itself. - /// The method returns the next occurrence after - /// . Also, is - /// exclusive. - /// - - public IEnumerable GetNextOccurrences(DateTime baseTime, DateTime endTime) + // + // Minute + // + + minute = this.minutes.Next(minute); + + if (minute == nil) { - for (var occurrence = TryGetNextOccurrence(baseTime, endTime); - occurrence != null && occurrence < endTime; - occurrence = TryGetNextOccurrence(occurrence.Value, endTime)) - { - yield return occurrence.Value; - } + second = seconds.GetFirst(); + minute = this.minutes.GetFirst(); + hour++; } - - /// - /// Gets the next occurrence of this schedule starting with a base time. - /// - - public DateTime GetNextOccurrence(DateTime baseTime) => - GetNextOccurrence(baseTime, DateTime.MaxValue); - - /// - /// Gets the next occurrence of this schedule starting with a base - /// time and up to an end time limit. - /// - /// - /// This method does not return the value of - /// itself if it falls on the schedule. For example, if - /// is midnight and the schedule was created from the expression * * * * * - /// (meaning every minute) then the next occurrence of the schedule - /// will be at one minute past midnight and not midnight itself. - /// The method returns the next occurrence after - /// . Also, is - /// exclusive. - /// - public DateTime GetNextOccurrence(DateTime baseTime, DateTime endTime) - => TryGetNextOccurrence(baseTime, endTime) ?? endTime; - - DateTime? TryGetNextOccurrence(DateTime baseTime, DateTime endTime) + else if (minute > baseMinute) { - const int nil = -1; - - var baseYear = baseTime.Year; - var baseMonth = baseTime.Month; - var baseDay = baseTime.Day; - var baseHour = baseTime.Hour; - var baseMinute = baseTime.Minute; - var baseSecond = baseTime.Second; - - var endYear = endTime.Year; - var endMonth = endTime.Month; - var endDay = endTime.Day; - - var year = baseYear; - var month = baseMonth; - var day = baseDay; - var hour = baseHour; - var minute = baseMinute; - var second = baseSecond + 1; - - // - // Second - // - - var seconds = _seconds ?? SecondZero; - second = seconds.Next(second); - - if (second == nil) - { - second = seconds.GetFirst(); - minute++; - } - - // - // Minute - // + second = seconds.GetFirst(); + } - minute = _minutes.Next(minute); + // + // Hour + // - if (minute == nil) - { - minute = _minutes.GetFirst(); - hour++; - } + hour = this.hours.Next(hour); - // - // Hour - // + if (hour == nil) + { + minute = this.minutes.GetFirst(); + hour = this.hours.GetFirst(); + day++; + } + else if (hour > baseHour) + { + second = seconds.GetFirst(); + minute = this.minutes.GetFirst(); + } - hour = _hours.Next(hour); + // + // Day + // - if (hour == nil) - { - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - day++; - } - else if (hour > baseHour) - { - minute = _minutes.GetFirst(); - } + day = this.days.Next(day); - // - // Day - // + RetryDayMonth: - day = _days.Next(day); + if (day == nil) + { + second = seconds.GetFirst(); + minute = this.minutes.GetFirst(); + hour = this.hours.GetFirst(); + day = this.days.GetFirst(); + month++; + } + else if (day > baseDay) + { + second = seconds.GetFirst(); + minute = this.minutes.GetFirst(); + hour = this.hours.GetFirst(); + } - RetryDayMonth: + // + // Month + // - if (day == nil) - { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - day = _days.GetFirst(); - month++; - } - else if (day > baseDay) - { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - } + month = this.months.Next(month); - // - // Month - // + if (month == nil) + { + second = seconds.GetFirst(); + minute = this.minutes.GetFirst(); + hour = this.hours.GetFirst(); + day = this.days.GetFirst(); + month = this.months.GetFirst(); + year++; + } + else if (month > baseMonth) + { + second = seconds.GetFirst(); + minute = this.minutes.GetFirst(); + hour = this.hours.GetFirst(); + day = this.days.GetFirst(); + } - month = _months.Next(month); + // + // Stop processing when year is too large for the datetime or calendar object. Otherwise we + // would get an exception. + // - if (month == nil) - { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - day = _days.GetFirst(); - month = _months.GetFirst(); - year++; - } - else if (month > baseMonth) - { - second = seconds.GetFirst(); - minute = _minutes.GetFirst(); - hour = _hours.GetFirst(); - day = _days.GetFirst(); - } - - // - // Stop processing when year is too large for the datetime or calendar - // object. Otherwise we would get an exception. - // - - if (year > Calendar.MaxSupportedDateTime.Year) - return null; - - // - // The day field in a cron expression spans the entire range of days - // in a month, which is from 1 to 31. However, the number of days in - // a month tend to be variable depending on the month (and the year - // in case of February). So a check is needed here to see if the - // date is a border case. If the day happens to be beyond 28 - // (meaning that we're dealing with the suspicious range of 29-31) - // and the date part has changed then we need to determine whether - // the day still makes sense for the given year and month. If the - // day is beyond the last possible value, then the day/month part - // for the schedule is re-evaluated. So an expression like "0 0 - // 15,31 * *" will yield the following sequence starting on midnight - // of Jan 1, 2000: - // - // Jan 15, Jan 31, Feb 15, Mar 15, Apr 15, Apr 31, ... - // - - var dateChanged = day != baseDay || month != baseMonth || year != baseYear; - - if (day > 28 && dateChanged && day > Calendar.GetDaysInMonth(year, month)) - { - if (year >= endYear && month >= endMonth && day >= endDay) - return endTime; + if (year > Calendar.MaxSupportedDateTime.Year) + return null; - day = nil; - goto RetryDayMonth; - } + // + // The day field in a cron expression spans the entire range of days in a month, which is + // from 1 to 31. However, the number of days in a month tend to be variable depending on the + // month (and the year in case of February). So a check is needed here to see if the date is + // a border case. If the day happens to be beyond 28 (meaning that we're dealing with the + // suspicious range of 29-31) and the date part has changed then we need to determine + // whether the day still makes sense for the given year and month. If the day is beyond the + // last possible value, then the day/month part for the schedule is re-evaluated. So an + // expression like "0 0 15,31 * *" will yield the following sequence starting on midnight of + // Jan 1, 2000: + // + // Jan 15, Jan 31, Feb 15, Mar 15, Apr 15, Apr 31, ... + // - var nextTime = new DateTime(year, month, day, hour, minute, second, 0, baseTime.Kind); + var dateChanged = day != baseDay || month != baseMonth || year != baseYear; - if (nextTime >= endTime) + if (day > 28 && dateChanged && day > Calendar.GetDaysInMonth(year, month)) + { + if (year >= endYear && month >= endMonth && day >= endDay) return endTime; - // - // Day of week - // + day = nil; + goto RetryDayMonth; + } - if (_daysOfWeek.Contains((int) nextTime.DayOfWeek)) - return nextTime; + var nextTime = new DateTime(year, month, day, hour, minute, second, 0, baseTime.Kind); - return TryGetNextOccurrence(new DateTime(year, month, day, 23, 59, 59, 0, baseTime.Kind), endTime); - } + if (nextTime >= endTime) + return endTime; - /// - /// Returns a string in crontab expression (expanded) that represents - /// this schedule. - /// + // + // Day of week + // - public override string ToString() +#pragma warning disable IDE0046 // Use conditional expression for return (readability) + if (this.daysOfWeek.Contains((int)nextTime.DayOfWeek)) +#pragma warning restore IDE0046 // Use conditional expression for return { - var writer = new StringWriter(CultureInfo.InvariantCulture); + return nextTime; + } - if (_seconds != null) - { - _seconds.Format(writer, true); - writer.Write(' '); - } - _minutes.Format(writer, true); writer.Write(' '); - _hours.Format(writer, true); writer.Write(' '); - _days.Format(writer, true); writer.Write(' '); - _months.Format(writer, true); writer.Write(' '); - _daysOfWeek.Format(writer, true); - - return writer.ToString(); + return TryGetNextOccurrence(new DateTime(year, month, day, 23, 59, 59, 0, baseTime.Kind), endTime); + } + + /// + /// Returns a string in crontab expression (expanded) that represents this schedule. + /// + + public override string ToString() + { + using var writer = new StringWriter(CultureInfo.InvariantCulture); + + if (this.seconds != null) + { + this.seconds.Format(writer, true); + writer.Write(' '); } + this.minutes.Format(writer, true); writer.Write(' '); + this.hours.Format(writer, true); writer.Write(' '); + this.days.Format(writer, true); writer.Write(' '); + this.months.Format(writer, true); writer.Write(' '); + this.daysOfWeek.Format(writer, true); - static Calendar Calendar => CultureInfo.InvariantCulture.Calendar; + return writer.ToString(); } + + static Calendar Calendar => CultureInfo.InvariantCulture.Calendar; } diff --git a/NCrontab/CrontabScheduleExtensions.cs b/NCrontab/CrontabScheduleExtensions.cs new file mode 100644 index 0000000..a19faae --- /dev/null +++ b/NCrontab/CrontabScheduleExtensions.cs @@ -0,0 +1,72 @@ +#region License and Terms +// +// NCrontab - Crontab for .NET +// Copyright (c) 2008 Atif Aziz. All rights reserved. +// Portions Copyright (c) 2023 Microsoft Corp. 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. +// +#endregion + +using System; +using System.Collections.Generic; + +namespace NCrontab; + +public static class CrontabScheduleExtensions +{ + /// + /// Generates a sequence of unique next occurrences (in order) between two dates based on one or + /// more schedules. + /// + /// + /// The and arguments are exclusive. + /// + + public static IEnumerable + GetNextOccurrences(this IEnumerable schedules, + DateTime baseTime, DateTime endTime) + { + if (schedules == null) throw new ArgumentNullException(nameof(schedules)); + + return GetNextOccurrences(schedules, baseTime, endTime, static (_, dt) => dt).DistinctUntilChanged(); + } + + /// + /// Generates a sequence of next occurrences (in order) between two dates and based on one or + /// more schedules. An additional parameter specifies a function that projects the items of the + /// resulting sequence where each invocation of the function is given the schedule that produced + /// the occurrence and the occurrence itself. + /// + /// + /// + /// The and arguments are + /// exclusive. + /// + /// The resulting sequence can contain duplicate occurrences if multiple schedules produce the + /// same occurrence. + /// + + public static IEnumerable + GetNextOccurrences(this IEnumerable schedules, + DateTime baseTime, DateTime endTime, + Func resultSelector) + { + if (schedules == null) throw new ArgumentNullException(nameof(schedules)); + if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); + + return Schedule.GetOccurrences(schedules, + s => s.GetNextOccurrences(baseTime, endTime), + resultSelector); + } +} diff --git a/NCrontab/ErrorHandling.cs b/NCrontab/ErrorHandling.cs index 9f97008..0b112e0 100644 --- a/NCrontab/ErrorHandling.cs +++ b/NCrontab/ErrorHandling.cs @@ -18,13 +18,12 @@ // #endregion -namespace NCrontab -{ - using System; +using System; - /// - /// Represents the method that will generate an object. - /// +namespace NCrontab; - public delegate Exception ExceptionProvider(); -} +/// +/// Represents the method that will generate an object. +/// + +public delegate Exception ExceptionProvider(); diff --git a/NCrontab/Extensions.cs b/NCrontab/Extensions.cs new file mode 100644 index 0000000..5c502ef --- /dev/null +++ b/NCrontab/Extensions.cs @@ -0,0 +1,62 @@ +#region License and Terms +// +// NCrontab - Crontab for .NET +// Copyright (c) 2024 Atif Aziz. All rights reserved. +// Portions Copyright (c) 2023 Microsoft Corp. 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. +// +#endregion + +using System; +using System.Collections.Generic; + +namespace NCrontab; + +static class Extensions +{ + /// + /// Iterates over the given sequence and yields the first occurrence of each consecutively + /// repeating element, e.g. [1, 2, 2, 3, 3, 3, 2, 4][1, 2, 3, 2, 4]. + /// + + public static IEnumerable DistinctUntilChanged(this IEnumerable source, + IEqualityComparer? comparer = null) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + + return Iterator(source, comparer ?? EqualityComparer.Default); + + static IEnumerable Iterator(IEnumerable source, IEqualityComparer comparer) + { + using var enumerator = source.GetEnumerator(); + + if (!enumerator.MoveNext()) + yield break; + + var running = enumerator.Current; + yield return running; + + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + + if (comparer.Equals(current, running)) + continue; + + running = current; + yield return current; + } + } + } +} diff --git a/NCrontab/ICrontabField.cs b/NCrontab/ICrontabField.cs index dbf862c..1ecfad8 100644 --- a/NCrontab/ICrontabField.cs +++ b/NCrontab/ICrontabField.cs @@ -18,12 +18,13 @@ // #endregion -namespace NCrontab +namespace NCrontab; + +public interface ICrontabField { - public interface ICrontabField - { - int GetFirst(); - int Next(int start); - bool Contains(int value); - } -} \ No newline at end of file + int GetFirst(); +#pragma warning disable CA1716 // Identifiers should not match keywords (by design) + int Next(int start); +#pragma warning restore CA1716 // Identifiers should not match keywords + bool Contains(int value); +} diff --git a/NCrontab/NCrontab.csproj b/NCrontab/NCrontab.csproj index 091b733..3378839 100644 --- a/NCrontab/NCrontab.csproj +++ b/NCrontab/NCrontab.csproj @@ -1,63 +1,23 @@ - + + + - NCrontab is crontab for all .NET runtimes supported by .NET Standard 1.0. It provides parsing and formatting of crontab expressions as well as calculation of occurrences of time based on a schedule expressed in the crontab format. - Copyright © 2008 Atif Aziz. All rights reserved. Portions Copyright © 2001 The OpenSymphony Group. All rights reserved. - NCrontab - en-US - 3.3.1 - Atif Aziz - net35;netstandard1.0;netstandard2.0 - true - portable NCrontab - Library + NCrontab NCrontab - cron;schedule;time - https://github.com/atifaziz/NCrontab - true - COPYING.txt - ..\dist - false - false - false - false - false - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - - - - - - - $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client - - - - - - - - $(DefineConstants);SERIALIZATION - - - - - - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + + diff --git a/NCrontab/NCrontab.props b/NCrontab/NCrontab.props new file mode 100644 index 0000000..79ff04f --- /dev/null +++ b/NCrontab/NCrontab.props @@ -0,0 +1,54 @@ + + + + NCrontab is crontab for all .NET runtimes supported by .NET Standard 1.0. It provides parsing and formatting of crontab expressions as well as calculation of occurrences of time based on a schedule expressed in the crontab format. + Copyright © 2008 Atif Aziz. All rights reserved. Portions Copyright © 2001 The OpenSymphony Group. All rights reserved. Portions Copyright © 2023 Microsoft Corp. All rights reserved. + en-US + 3.4.0 + Atif Aziz + netstandard2.0;netstandard1.0;net35 + 12 + Library + CS1591;CS1573 + True + cron;schedule;time + https://github.com/atifaziz/NCrontab + true + COPYING.txt + README.md + ..\dist + false + false + false + false + false + + + + + + + + + + + + + + $(DefineConstants);SERIALIZATION + + + + + + + + + + + + + + + + diff --git a/NCrontab/PublicAPI/net35/PublicAPI.Shipped.txt b/NCrontab/PublicAPI/net35/PublicAPI.Shipped.txt new file mode 100644 index 0000000..2f3239a --- /dev/null +++ b/NCrontab/PublicAPI/net35/PublicAPI.Shipped.txt @@ -0,0 +1,50 @@ +#nullable enable +NCrontab.CrontabException +NCrontab.CrontabException.CrontabException() -> void +NCrontab.CrontabException.CrontabException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void +NCrontab.CrontabException.CrontabException(string? message) -> void +NCrontab.CrontabException.CrontabException(string? message, System.Exception? innerException) -> void +NCrontab.CrontabField +NCrontab.CrontabField.Contains(int value) -> bool +NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void +NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void +NCrontab.CrontabField.GetFirst() -> int +NCrontab.CrontabField.Next(int start) -> int +NCrontab.CrontabField.ToString(string? format) -> string! +NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.DayOfWeek = 5 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Hour = 2 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Minute = 1 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Month = 4 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Second = 0 -> NCrontab.CrontabFieldKind +NCrontab.CrontabSchedule +NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime +NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime +NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +NCrontab.CrontabSchedule.ParseOptions +NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool +NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void +NCrontab.CrontabSchedule.ParseOptions.ParseOptions() -> void +NCrontab.ExceptionProvider +NCrontab.ICrontabField +NCrontab.ICrontabField.Contains(int value) -> bool +NCrontab.ICrontabField.GetFirst() -> int +NCrontab.ICrontabField.Next(int start) -> int +override NCrontab.CrontabField.ToString() -> string! +override NCrontab.CrontabSchedule.ToString() -> string! +static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.DaysOfWeek(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Hours(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Minutes(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Months(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Parse(NCrontab.CrontabFieldKind kind, string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Seconds(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.TryParse(NCrontab.CrontabFieldKind kind, string! expression) -> NCrontab.CrontabField? +static NCrontab.CrontabField.TryParse(NCrontab.CrontabFieldKind kind, string! expression, System.Func! valueSelector, System.Func! errorSelector) -> T +static NCrontab.CrontabSchedule.Parse(string! expression) -> NCrontab.CrontabSchedule! +static NCrontab.CrontabSchedule.Parse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options) -> NCrontab.CrontabSchedule! +static NCrontab.CrontabSchedule.TryParse(string! expression) -> NCrontab.CrontabSchedule? +static NCrontab.CrontabSchedule.TryParse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options) -> NCrontab.CrontabSchedule? +static NCrontab.CrontabSchedule.TryParse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options, System.Func! valueSelector, System.Func! errorSelector) -> T +static NCrontab.CrontabSchedule.TryParse(string! expression, System.Func! valueSelector, System.Func! errorSelector) -> T diff --git a/NCrontab/PublicAPI/net35/PublicAPI.Unshipped.txt b/NCrontab/PublicAPI/net35/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..96e5033 --- /dev/null +++ b/NCrontab/PublicAPI/net35/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +#nullable enable +NCrontab.CrontabScheduleExtensions +static NCrontab.CrontabScheduleExtensions.GetNextOccurrences(this System.Collections.Generic.IEnumerable! schedules, System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +static NCrontab.CrontabScheduleExtensions.GetNextOccurrences(this System.Collections.Generic.IEnumerable! schedules, System.DateTime baseTime, System.DateTime endTime, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Shipped.txt b/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..2997735 --- /dev/null +++ b/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Shipped.txt @@ -0,0 +1,49 @@ +#nullable enable +NCrontab.CrontabException +NCrontab.CrontabException.CrontabException() -> void +NCrontab.CrontabException.CrontabException(string? message) -> void +NCrontab.CrontabException.CrontabException(string? message, System.Exception? innerException) -> void +NCrontab.CrontabField +NCrontab.CrontabField.Contains(int value) -> bool +NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void +NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void +NCrontab.CrontabField.GetFirst() -> int +NCrontab.CrontabField.Next(int start) -> int +NCrontab.CrontabField.ToString(string? format) -> string! +NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.DayOfWeek = 5 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Hour = 2 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Minute = 1 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Month = 4 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Second = 0 -> NCrontab.CrontabFieldKind +NCrontab.CrontabSchedule +NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime +NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime +NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +NCrontab.CrontabSchedule.ParseOptions +NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool +NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void +NCrontab.CrontabSchedule.ParseOptions.ParseOptions() -> void +NCrontab.ExceptionProvider +NCrontab.ICrontabField +NCrontab.ICrontabField.Contains(int value) -> bool +NCrontab.ICrontabField.GetFirst() -> int +NCrontab.ICrontabField.Next(int start) -> int +override NCrontab.CrontabField.ToString() -> string! +override NCrontab.CrontabSchedule.ToString() -> string! +static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.DaysOfWeek(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Hours(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Minutes(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Months(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Parse(NCrontab.CrontabFieldKind kind, string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Seconds(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.TryParse(NCrontab.CrontabFieldKind kind, string! expression) -> NCrontab.CrontabField? +static NCrontab.CrontabField.TryParse(NCrontab.CrontabFieldKind kind, string! expression, System.Func! valueSelector, System.Func! errorSelector) -> T +static NCrontab.CrontabSchedule.Parse(string! expression) -> NCrontab.CrontabSchedule! +static NCrontab.CrontabSchedule.Parse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options) -> NCrontab.CrontabSchedule! +static NCrontab.CrontabSchedule.TryParse(string! expression) -> NCrontab.CrontabSchedule? +static NCrontab.CrontabSchedule.TryParse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options) -> NCrontab.CrontabSchedule? +static NCrontab.CrontabSchedule.TryParse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options, System.Func! valueSelector, System.Func! errorSelector) -> T +static NCrontab.CrontabSchedule.TryParse(string! expression, System.Func! valueSelector, System.Func! errorSelector) -> T diff --git a/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Unshipped.txt b/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..96e5033 --- /dev/null +++ b/NCrontab/PublicAPI/netstandard1.0/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +#nullable enable +NCrontab.CrontabScheduleExtensions +static NCrontab.CrontabScheduleExtensions.GetNextOccurrences(this System.Collections.Generic.IEnumerable! schedules, System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +static NCrontab.CrontabScheduleExtensions.GetNextOccurrences(this System.Collections.Generic.IEnumerable! schedules, System.DateTime baseTime, System.DateTime endTime, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..2997735 --- /dev/null +++ b/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt @@ -0,0 +1,49 @@ +#nullable enable +NCrontab.CrontabException +NCrontab.CrontabException.CrontabException() -> void +NCrontab.CrontabException.CrontabException(string? message) -> void +NCrontab.CrontabException.CrontabException(string? message, System.Exception? innerException) -> void +NCrontab.CrontabField +NCrontab.CrontabField.Contains(int value) -> bool +NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void +NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void +NCrontab.CrontabField.GetFirst() -> int +NCrontab.CrontabField.Next(int start) -> int +NCrontab.CrontabField.ToString(string? format) -> string! +NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.DayOfWeek = 5 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Hour = 2 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Minute = 1 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Month = 4 -> NCrontab.CrontabFieldKind +NCrontab.CrontabFieldKind.Second = 0 -> NCrontab.CrontabFieldKind +NCrontab.CrontabSchedule +NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime +NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime +NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +NCrontab.CrontabSchedule.ParseOptions +NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool +NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void +NCrontab.CrontabSchedule.ParseOptions.ParseOptions() -> void +NCrontab.ExceptionProvider +NCrontab.ICrontabField +NCrontab.ICrontabField.Contains(int value) -> bool +NCrontab.ICrontabField.GetFirst() -> int +NCrontab.ICrontabField.Next(int start) -> int +override NCrontab.CrontabField.ToString() -> string! +override NCrontab.CrontabSchedule.ToString() -> string! +static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.DaysOfWeek(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Hours(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Minutes(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Months(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Parse(NCrontab.CrontabFieldKind kind, string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.Seconds(string! expression) -> NCrontab.CrontabField! +static NCrontab.CrontabField.TryParse(NCrontab.CrontabFieldKind kind, string! expression) -> NCrontab.CrontabField? +static NCrontab.CrontabField.TryParse(NCrontab.CrontabFieldKind kind, string! expression, System.Func! valueSelector, System.Func! errorSelector) -> T +static NCrontab.CrontabSchedule.Parse(string! expression) -> NCrontab.CrontabSchedule! +static NCrontab.CrontabSchedule.Parse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options) -> NCrontab.CrontabSchedule! +static NCrontab.CrontabSchedule.TryParse(string! expression) -> NCrontab.CrontabSchedule? +static NCrontab.CrontabSchedule.TryParse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options) -> NCrontab.CrontabSchedule? +static NCrontab.CrontabSchedule.TryParse(string! expression, NCrontab.CrontabSchedule.ParseOptions? options, System.Func! valueSelector, System.Func! errorSelector) -> T +static NCrontab.CrontabSchedule.TryParse(string! expression, System.Func! valueSelector, System.Func! errorSelector) -> T diff --git a/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..96e5033 --- /dev/null +++ b/NCrontab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +#nullable enable +NCrontab.CrontabScheduleExtensions +static NCrontab.CrontabScheduleExtensions.GetNextOccurrences(this System.Collections.Generic.IEnumerable! schedules, System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable! +static NCrontab.CrontabScheduleExtensions.GetNextOccurrences(this System.Collections.Generic.IEnumerable! schedules, System.DateTime baseTime, System.DateTime endTime, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/NCrontab/Schedule.cs b/NCrontab/Schedule.cs new file mode 100644 index 0000000..83b101d --- /dev/null +++ b/NCrontab/Schedule.cs @@ -0,0 +1,130 @@ +#region License and Terms +// +// NCrontab - Crontab for .NET +// Copyright (c) 2008 Atif Aziz. All rights reserved. +// Portions Copyright (c) 2001 The OpenSymphony Group. 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. +// +#endregion + +using System.Collections.Generic; +using System.Linq; +using System; + +namespace NCrontab; + +static class Schedule +{ + /// + /// Generates a sequence of occurrences based on one or more schedules. + /// + + public static IEnumerable + GetOccurrences(IEnumerable source, + Func> occurrencesSelector, + Func resultSelector) + { + return from e in source.Aggregate(Enumerable.Empty>(), + (a, s) => a.Merge(from e in occurrencesSelector(s) + select Pair(s, e))) + select resultSelector(e.Key, e.Value); + + static KeyValuePair Pair(TKey key, TValue value) => new(key, value); + } + + enum Sides { None, First, Second, Both } + + /// + /// The schedules, and , are expected + /// to produce a timeline of unique occurrences in order of earliest to latest time. The + /// behaviour is otherwise undefined. + /// + + static IEnumerable> + Merge(this IEnumerable> schedule1, + IEnumerable> schedule2) + { + // Initialize two enumerators for each input sequence. + + using var enumerator1 = schedule1.GetEnumerator(); + using var enumerator2 = schedule2.GetEnumerator(); + + // Initialize the flags to determine if each enumerator has a value. + + var have1 = enumerator1.MoveNext(); + var have2 = enumerator2.MoveNext(); + + // Enumerate and yield the items in order of earliest to latest time. + + for (;;) + { + // Determine which sequence contains the next smallest due time. + + var sides = have1 && have2 ? Sides.Both + : have1 ? Sides.First + : have2 ? Sides.Second + : Sides.None; + +#pragma warning disable IDE0010 // Add missing cases (false negative) + switch (sides) +#pragma warning restore IDE0010 // Add missing cases + { + case Sides.First: // Only first sequence has a value. + { + yield return enumerator1.Current; + have1 = enumerator1.MoveNext(); + break; + } + case Sides.Second: // Only second sequence has a value. + { + yield return enumerator2.Current; + have2 = enumerator2.MoveNext(); + break; + } + case Sides.Both: // Both sequences have a value. + { + // Determine which of the two has the next earliest time. + + var occurrence1 = enumerator1.Current; + var occurrence2 = enumerator2.Current; + + if (occurrence1.Value.CompareTo(occurrence2.Value) > 0) + { + // Second has the earlier time, yield it and progress to the next value in + // the second sequence. + + yield return occurrence2; + have2 = enumerator2.MoveNext(); + } + else + { + // First has the earlier time, yield it and progress to the next value in + // the first sequence. + + yield return occurrence1; + have1 = enumerator1.MoveNext(); + } + + break; + } + case Sides.None: + { + // No value left in either sequence, so end enumerating. + + yield break; + } + } + } + } +} diff --git a/NCrontab/Serialization.cs b/NCrontab/Serialization.cs index 7e3698e..dabdb6a 100644 --- a/NCrontab/Serialization.cs +++ b/NCrontab/Serialization.cs @@ -20,33 +20,32 @@ #if SERIALIZATION -namespace NCrontab -{ - using System; - using System.Runtime.Serialization; +using System; +using System.Runtime.Serialization; - [Serializable] - partial class CrontabException - { - protected CrontabException(SerializationInfo info, StreamingContext context) : - base(info, context) {} - } +namespace NCrontab; - [Serializable] - partial class CrontabField {} +[Serializable] +partial class CrontabException +{ + protected CrontabException(SerializationInfo info, StreamingContext context) : + base(info, context) { } +} - [Serializable] - partial class CrontabSchedule - { - [Serializable] - partial class ParseOptions {} - } +[Serializable] +partial class CrontabField { } +[Serializable] +partial class CrontabSchedule +{ [Serializable] - partial class CrontabFieldImpl : IObjectReference - { - object IObjectReference.GetRealObject(StreamingContext context) => FromKind(Kind); - } + partial class ParseOptions { } +} + +[Serializable] +partial class CrontabFieldImpl : IObjectReference +{ + object IObjectReference.GetRealObject(StreamingContext context) => FromKind(Kind); } #endif diff --git a/NCrontab/StringSeparatorStock.cs b/NCrontab/StringSeparatorStock.cs index a65871a..17dfad1 100644 --- a/NCrontab/StringSeparatorStock.cs +++ b/NCrontab/StringSeparatorStock.cs @@ -17,11 +17,10 @@ // #endregion -namespace NCrontab +namespace NCrontab; + +static class StringSeparatorStock { - static class StringSeparatorStock - { - public static readonly char[] Space = { ' ' }; - public static readonly char[] Comma = { ',' }; - } -} \ No newline at end of file + public static readonly char[] Space = [' ']; + public static readonly char[] Comma = [',']; +} diff --git a/NCrontabConsole/App.config b/NCrontabConsole/App.config deleted file mode 100644 index 2e1d53e..0000000 --- a/NCrontabConsole/App.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NCrontabConsole/AssemblyInfo.g.cs b/NCrontabConsole/AssemblyInfo.g.cs index 5a4923f..0cca03c 100644 --- a/NCrontabConsole/AssemblyInfo.g.cs +++ b/NCrontabConsole/AssemblyInfo.g.cs @@ -1,8 +1,8 @@ // This code was generated by a tool. Any changes made manually will be lost // the next time this code is regenerated. -// Generated: Wed, 21 Dec 2016 08:43:43 GMT - +// Generated: Sun, 28 Jul 2024 14:19:55 GMT + using System.Reflection; - -[assembly: AssemblyVersion("3.3.20321.0")] -[assembly: AssemblyFileVersion("3.3.20321.843")] \ No newline at end of file + +[assembly: AssemblyVersion("3.4.29428.0")] +[assembly: AssemblyFileVersion("3.4.29428.1419")] diff --git a/NCrontabConsole/AssemblyInfo.g.tt b/NCrontabConsole/AssemblyInfo.g.tt index b8495ff..5687e85 100644 --- a/NCrontabConsole/AssemblyInfo.g.tt +++ b/NCrontabConsole/AssemblyInfo.g.tt @@ -8,8 +8,8 @@ // This code was generated by a tool. Any changes made manually will be lost // the next time this code is regenerated. // Generated: <#= date.ToString("r") #> - + using System.Reflection; - -[assembly: AssemblyVersion("3.3.<#= build #>.0")] -[assembly: AssemblyFileVersion("3.3.<#= build #>.<#= revision #>")] \ No newline at end of file + +[assembly: AssemblyVersion("3.4.<#= build #>.0")] +[assembly: AssemblyFileVersion("3.4.<#= build #>.<#= revision #>")] diff --git a/NCrontabConsole/NCrontabConsole.csproj b/NCrontabConsole/NCrontabConsole.csproj index fb801f0..8757172 100644 --- a/NCrontabConsole/NCrontabConsole.csproj +++ b/NCrontabConsole/NCrontabConsole.csproj @@ -1,11 +1,9 @@ - + - netcoreapp1.0 + net8.0 portable Exe - $(PackageTargetFallback);dnxcore50 - 1.0.4 false false false @@ -18,11 +16,29 @@ - + + + TextTemplatingFileGenerator + AssemblyInfo.g.cs + + + + + + + + + + True + True + AssemblyInfo.g.tt + + + diff --git a/NCrontabConsole/Program.cs b/NCrontabConsole/Program.cs index 845a44e..dd94440 100644 --- a/NCrontabConsole/Program.cs +++ b/NCrontabConsole/Program.cs @@ -17,83 +17,93 @@ // #endregion -namespace NCrontabConsole -{ - #region Imports +#pragma warning disable CA1852 // Seal internal types (incorrect) + // Type 'Program' can be sealed because it has no subtypes in its + // containing assembly and is not externally visible. + // See: https://github.com/dotnet/roslyn-analyzers/issues/6141 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using NCrontab; - using System; - using System.Collections.Generic; - using System.Globalization; - using NCrontab; +var verbose = false; - #endregion +try +{ + var argList = new List(args); + var verboseIndex = argList.IndexOf("--verbose") is var vi and >= 0 ? vi + : argList.IndexOf("-v"); + // ReSharper disable once AssignmentInConditionalExpression + if (verbose = verboseIndex >= 0) + argList.RemoveAt(verboseIndex); - static class Program - { - static int Main(string[] args) + var (expression, startTimeString, endTimeString, format) = + argList switch { - var verbose = false; + [var exa, var sta, var eta] => (exa, sta, eta, null), + [var exa, var sta, var eta, var fma, ..] => (exa, sta, eta, fma), + _ => throw new ApplicationException("Missing required arguments. You must at least supply CRONTAB-EXPRESSION START-DATE END-DATE."), + }; - try - { - var argList = new List(args); - var verboseIndex = argList.IndexOf("--verbose"); - // ReSharper disable once AssignmentInConditionalExpression - if (verbose = verboseIndex >= 0) - argList.RemoveAt(verboseIndex); + var expressions = expression.Split(';') + .Select(s => s.Trim()) + .Where(s => s.Length > 0) + .ToArray(); - if (argList.Count < 3) - throw new ApplicationException("Missing required arguments. You must at least supply CRONTAB-EXPRESSION START-DATE END-DATE."); + var start = ParseDateArgument(startTimeString, "start"); + var end = ParseDateArgument(endTimeString, "end"); - var expression = argList[0].Trim(); - var options = new CrontabSchedule.ParseOptions + var exopts = + expressions + .Select(expression => new + { + Expression = expression, + Options = new CrontabSchedule.ParseOptions { - IncludingSeconds = expression.Split(' ').Length > 5, - }; + IncludingSeconds = expression.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Length == 6, + }, + }) + .ToArray(); - var start = ParseDateArgument(argList[1], "start"); - var end = ParseDateArgument(argList[2], "end"); - var format = - argList.Count > 3 ? argList[3] - : options.IncludingSeconds ? "ddd, dd MMM yyyy HH:mm:ss" - : "ddd, dd MMM yyyy HH:mm"; + format ??= exopts.Any(e => e.Options.IncludingSeconds) + ? "ddd, dd MMM yyyy HH:mm:ss" + : "ddd, dd MMM yyyy HH:mm"; - var schedule = CrontabSchedule.Parse(expression, options); + var schedules = + from e in exopts + select CrontabSchedule.Parse(e.Expression, e.Options); - foreach (var occurrence in schedule.GetNextOccurrences(start, end)) - Console.Out.WriteLine(occurrence.ToString(format)); + foreach (var occurrence in schedules.GetNextOccurrences(start, end)) + Console.Out.WriteLine(occurrence.ToString(format, null)); - return 0; - } - catch (Exception e) - { - var error = - verbose - ? e.ToString() - : e is ApplicationException - ? e.Message : e.GetBaseException().Message; - Console.Error.WriteLine(error); - return 1; - } - } + return 0; +} +#pragma warning disable CA1031 // Do not catch general exception types +catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types +{ + var error = + verbose + ? e.ToString() + : e is ApplicationException + ? e.Message + : e.GetBaseException().Message; + Console.Error.WriteLine(error); + return 1; +} - static DateTime ParseDateArgument(string arg, string hint) - { - try - { - return DateTime.Parse(arg, null, DateTimeStyles.AssumeLocal); - } - catch (FormatException e) - { - throw new ApplicationException("Invalid " + hint + " date or date format argument.", e); - } - } +static DateTime ParseDateArgument(string arg, string hint) + => DateTime.TryParse(arg, null, DateTimeStyles.AssumeLocal, out var v) ? v + : throw new ApplicationException("Invalid " + hint + " date or date format argument."); - sealed class ApplicationException : Exception - { - public ApplicationException() {} - public ApplicationException(string message) : base(message) {} - public ApplicationException(string message, Exception inner) : base(message, inner) {} - } - } +#pragma warning disable CA1064 // Exceptions should be public +sealed class ApplicationException(string? message, Exception? innerException) : + Exception(message, innerException) +#pragma warning restore CA1064 // Exceptions should be public +{ + public ApplicationException() : this(null) { } + public ApplicationException(string? message) : this(message, null) { } } diff --git a/NCrontabViewer/.editorconfig b/NCrontabViewer/.editorconfig new file mode 100644 index 0000000..ea1ae76 --- /dev/null +++ b/NCrontabViewer/.editorconfig @@ -0,0 +1,11 @@ +# http://editorconfig.org/ + +[*.cs] + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = suggestion + +[*Form*.cs] + +# IDE0021: Use expression body for constructor +dotnet_diagnostic.IDE0021.severity = suggestion diff --git a/NCrontabViewer/App.config b/NCrontabViewer/App.config deleted file mode 100644 index 2e1d53e..0000000 --- a/NCrontabViewer/App.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NCrontabViewer/AssemblyInfo.g.cs b/NCrontabViewer/AssemblyInfo.g.cs index 2ce5a42..f17307d 100644 --- a/NCrontabViewer/AssemblyInfo.g.cs +++ b/NCrontabViewer/AssemblyInfo.g.cs @@ -5,4 +5,4 @@ using System.Reflection; [assembly: AssemblyVersion("3.3.20321.0")] -[assembly: AssemblyFileVersion("3.3.20321.843")] \ No newline at end of file +[assembly: AssemblyFileVersion("3.3.20321.843")] diff --git a/NCrontabViewer/AssemblyInfo.g.tt b/NCrontabViewer/AssemblyInfo.g.tt index b53ddda..3dc09c1 100644 --- a/NCrontabViewer/AssemblyInfo.g.tt +++ b/NCrontabViewer/AssemblyInfo.g.tt @@ -12,4 +12,4 @@ using System.Reflection; [assembly: AssemblyVersion("3.3.<#= build #>.0")] -[assembly: AssemblyFileVersion("3.3.<#= build #>.<#= revision #>")] \ No newline at end of file +[assembly: AssemblyFileVersion("3.3.<#= build #>.<#= revision #>")] diff --git a/NCrontabViewer/MainForm.Designer.cs b/NCrontabViewer/MainForm.Designer.cs index e6d51af..20e421c 100644 --- a/NCrontabViewer/MainForm.Designer.cs +++ b/NCrontabViewer/MainForm.Designer.cs @@ -29,154 +29,150 @@ protected override void Dispose(bool disposing) private void InitializeComponent() { this.components = new System.ComponentModel.Container(); - this._startTimePicker = new System.Windows.Forms.DateTimePicker(); - this._endTimePicker = new System.Windows.Forms.DateTimePicker(); - this._timer = new System.Windows.Forms.Timer(this.components); - this._resultBox = new System.Windows.Forms.RichTextBox(); - this._cronBox = new System.Windows.Forms.TextBox(); + this.startTimePicker = new System.Windows.Forms.DateTimePicker(); + this.endTimePicker = new System.Windows.Forms.DateTimePicker(); + this.timer = new System.Windows.Forms.Timer(this.components); + this.resultBox = new System.Windows.Forms.RichTextBox(); + this.cronBox = new System.Windows.Forms.TextBox(); this.label1 = new System.Windows.Forms.Label(); this.label2 = new System.Windows.Forms.Label(); this.label3 = new System.Windows.Forms.Label(); - this._moreButton = new System.Windows.Forms.Button(); - this._statusBar = new System.Windows.Forms.StatusBar(); - this._statusBarPanel = new System.Windows.Forms.StatusBarPanel(); - this._errorProvider = new System.Windows.Forms.ErrorProvider(this.components); - ((System.ComponentModel.ISupportInitialize)(this._statusBarPanel)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this._errorProvider)).BeginInit(); + this.moreButton = new System.Windows.Forms.Button(); + this.statusBar = new System.Windows.Forms.StatusStrip(); + this.statusBarPanel = new System.Windows.Forms.ToolStripStatusLabel(); + this.errorProvider = new System.Windows.Forms.ErrorProvider(this.components); + ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).BeginInit(); this.SuspendLayout(); - // + // // _startTimePicker - // - this._startTimePicker.CustomFormat = "dd/MM/yyyy HH:mm"; - this._startTimePicker.Format = System.Windows.Forms.DateTimePickerFormat.Custom; - this._startTimePicker.Location = new System.Drawing.Point(65, 16); - this._startTimePicker.Name = "_startTimePicker"; - this._startTimePicker.Size = new System.Drawing.Size(166, 27); - this._startTimePicker.TabIndex = 0; - this._startTimePicker.ValueChanged += new System.EventHandler(this.CronBox_Changed); - // + // + this.startTimePicker.CustomFormat = "dd/MM/yyyy HH:mm"; + this.startTimePicker.Format = System.Windows.Forms.DateTimePickerFormat.Custom; + this.startTimePicker.Location = new System.Drawing.Point(65, 16); + this.startTimePicker.Name = "startTimePicker"; + this.startTimePicker.Size = new System.Drawing.Size(166, 27); + this.startTimePicker.TabIndex = 0; + this.startTimePicker.ValueChanged += new System.EventHandler(this.CronBox_Changed); + // // _endTimePicker - // - this._endTimePicker.CustomFormat = "dd/MM/yyyy HH:mm"; - this._endTimePicker.Format = System.Windows.Forms.DateTimePickerFormat.Custom; - this._endTimePicker.Location = new System.Drawing.Point(298, 16); - this._endTimePicker.Name = "_endTimePicker"; - this._endTimePicker.Size = new System.Drawing.Size(166, 27); - this._endTimePicker.TabIndex = 1; - this._endTimePicker.ValueChanged += new System.EventHandler(this.CronBox_Changed); - // + // + this.endTimePicker.CustomFormat = "dd/MM/yyyy HH:mm"; + this.endTimePicker.Format = System.Windows.Forms.DateTimePickerFormat.Custom; + this.endTimePicker.Location = new System.Drawing.Point(298, 16); + this.endTimePicker.Name = "endTimePicker"; + this.endTimePicker.Size = new System.Drawing.Size(166, 27); + this.endTimePicker.TabIndex = 1; + this.endTimePicker.ValueChanged += new System.EventHandler(this.CronBox_Changed); + // // _timer - // - this._timer.Enabled = true; - this._timer.Interval = 500; - this._timer.Tick += new System.EventHandler(this.Timer_Tick); - // + // + this.timer.Enabled = true; + this.timer.Interval = 500; + this.timer.Tick += new System.EventHandler(this.Timer_Tick); + // // _resultBox - // - this._resultBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) + // + this.resultBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this._resultBox.BackColor = System.Drawing.SystemColors.Control; - this._resultBox.Font = new System.Drawing.Font("Consolas", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this._resultBox.Location = new System.Drawing.Point(16, 88); - this._resultBox.Name = "_resultBox"; - this._resultBox.ReadOnly = true; - this._resultBox.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Vertical; - this._resultBox.Size = new System.Drawing.Size(531, 344); - this._resultBox.TabIndex = 3; - this._resultBox.Text = ""; - // + this.resultBox.BackColor = System.Drawing.SystemColors.Control; + this.resultBox.Font = new System.Drawing.Font("Consolas", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.resultBox.Location = new System.Drawing.Point(16, 88); + this.resultBox.Name = "resultBox"; + this.resultBox.ReadOnly = true; + this.resultBox.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Vertical; + this.resultBox.Size = new System.Drawing.Size(531, 344); + this.resultBox.TabIndex = 3; + this.resultBox.Text = ""; + // // _cronBox - // - this._cronBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + // + this.cronBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this._cronBox.Location = new System.Drawing.Point(143, 48); - this._cronBox.Name = "_cronBox"; - this._cronBox.Size = new System.Drawing.Size(323, 27); - this._cronBox.TabIndex = 2; - this._cronBox.Text = "* * * * *"; - this._cronBox.TextChanged += new System.EventHandler(this.CronBox_Changed); - // + this.cronBox.Location = new System.Drawing.Point(143, 48); + this.cronBox.Name = "cronBox"; + this.cronBox.Size = new System.Drawing.Size(323, 27); + this.cronBox.TabIndex = 2; + this.cronBox.Text = "* * * * *"; + this.cronBox.TextChanged += new System.EventHandler(this.CronBox_Changed); + // // label1 - // + // this.label1.AutoSize = true; this.label1.Location = new System.Drawing.Point(16, 16); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(43, 20); this.label1.TabIndex = 4; this.label1.Text = "&Start:"; - // + // // label2 - // + // this.label2.AutoSize = true; this.label2.Location = new System.Drawing.Point(255, 16); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(37, 20); this.label2.TabIndex = 5; this.label2.Text = "&End:"; - // + // // label3 - // + // this.label3.AutoSize = true; this.label3.Location = new System.Drawing.Point(16, 48); this.label3.Name = "label3"; this.label3.Size = new System.Drawing.Size(126, 20); this.label3.TabIndex = 6; this.label3.Text = "&Recurring Pattern:"; - // + // // _moreButton - // - this._moreButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this._moreButton.Enabled = false; - this._moreButton.Location = new System.Drawing.Point(484, 48); - this._moreButton.Name = "_moreButton"; - this._moreButton.Size = new System.Drawing.Size(63, 27); - this._moreButton.TabIndex = 7; - this._moreButton.Text = "&More"; - this._moreButton.Click += new System.EventHandler(this.More_Click); - // + // + this.moreButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.moreButton.Enabled = false; + this.moreButton.Location = new System.Drawing.Point(484, 48); + this.moreButton.Name = "moreButton"; + this.moreButton.Size = new System.Drawing.Size(63, 27); + this.moreButton.TabIndex = 7; + this.moreButton.Text = "&More"; + this.moreButton.Click += new System.EventHandler(this.More_Click); + // // _statusBar - // - this._statusBar.Location = new System.Drawing.Point(0, 448); - this._statusBar.Name = "_statusBar"; - this._statusBar.Panels.AddRange(new System.Windows.Forms.StatusBarPanel[] { - this._statusBarPanel}); - this._statusBar.ShowPanels = true; - this._statusBar.Size = new System.Drawing.Size(563, 24); - this._statusBar.TabIndex = 8; - this._statusBar.Text = "Ready"; - // + // + this.statusBar.Location = new System.Drawing.Point(0, 448); + this.statusBar.Name = "statusBar"; + this.statusBar.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.statusBarPanel}); + this.statusBar.Size = new System.Drawing.Size(563, 24); + this.statusBar.TabIndex = 8; + this.statusBar.Text = "Ready"; + // // _statusBarPanel - // - this._statusBarPanel.AutoSize = System.Windows.Forms.StatusBarPanelAutoSize.Spring; - this._statusBarPanel.Name = "_statusBarPanel"; - this._statusBarPanel.Text = "Ready"; - this._statusBarPanel.Width = 542; - // + // + this.statusBarPanel.AutoSize = true; + this.statusBarPanel.Name = "statusBarPanel"; + this.statusBarPanel.Text = "Ready"; + // // _errorProvider - // - this._errorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.AlwaysBlink; - this._errorProvider.ContainerControl = this; - // + // + this.errorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.AlwaysBlink; + this.errorProvider.ContainerControl = this; + // // MainForm - // + // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(563, 472); - this.Controls.Add(this._statusBar); - this.Controls.Add(this._moreButton); + this.Controls.Add(this.statusBar); + this.Controls.Add(this.moreButton); this.Controls.Add(this.label3); this.Controls.Add(this.label2); this.Controls.Add(this.label1); - this.Controls.Add(this._cronBox); - this.Controls.Add(this._resultBox); - this.Controls.Add(this._startTimePicker); - this.Controls.Add(this._endTimePicker); + this.Controls.Add(this.cronBox); + this.Controls.Add(this.resultBox); + this.Controls.Add(this.startTimePicker); + this.Controls.Add(this.endTimePicker); this.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.Name = "MainForm"; this.Text = "Crontab Viewer"; - ((System.ComponentModel.ISupportInitialize)(this._statusBarPanel)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this._errorProvider)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).EndInit(); this.ResumeLayout(false); this.PerformLayout(); @@ -184,17 +180,17 @@ private void InitializeComponent() #endregion - private System.Windows.Forms.DateTimePicker _startTimePicker; - private System.Windows.Forms.DateTimePicker _endTimePicker; - private System.Windows.Forms.Timer _timer; - private System.Windows.Forms.RichTextBox _resultBox; - private System.Windows.Forms.TextBox _cronBox; + private System.Windows.Forms.DateTimePicker startTimePicker; + private System.Windows.Forms.DateTimePicker endTimePicker; + private System.Windows.Forms.Timer timer; + private System.Windows.Forms.RichTextBox resultBox; + private System.Windows.Forms.TextBox cronBox; private System.Windows.Forms.Label label1; private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label3; - private System.Windows.Forms.Button _moreButton; - private System.Windows.Forms.StatusBar _statusBar; - private System.Windows.Forms.StatusBarPanel _statusBarPanel; - private System.Windows.Forms.ErrorProvider _errorProvider; + private System.Windows.Forms.Button moreButton; + private System.Windows.Forms.StatusStrip statusBar; + private System.Windows.Forms.ToolStripStatusLabel statusBarPanel; + private System.Windows.Forms.ErrorProvider errorProvider; } -} \ No newline at end of file +} diff --git a/NCrontabViewer/MainForm.cs b/NCrontabViewer/MainForm.cs index 04dd153..a42bbe4 100644 --- a/NCrontabViewer/MainForm.cs +++ b/NCrontabViewer/MainForm.cs @@ -17,176 +17,175 @@ // #endregion -namespace NCrontabViewer -{ - #region Imports +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Windows.Forms; +using NCrontab; + +namespace NCrontabViewer; - using System; - using System.Globalization; - using System.Linq; - using System.Text; - using System.Windows.Forms; - using NCrontab; +public partial class MainForm : Form +{ + static readonly char[] Separators = [' ']; - #endregion + DateTime lastChangeTime; + bool dirty; + CrontabSchedule? crontab; + bool isSixPart; + DateTime startTime; + int totalOccurrenceCount; - public partial class MainForm : Form + public MainForm() { - static readonly char[] Separators = { ' ' }; + InitializeComponent(); + } - DateTime _lastChangeTime; - bool _dirty; - CrontabSchedule _crontab; - bool _isSixPart; - DateTime _startTime; - int _totalOccurrenceCount; + // ReSharper disable once InconsistentNaming + void CronBox_Changed(object sender, EventArgs args) + { + this.lastChangeTime = DateTime.Now; + this.dirty = true; + this.isSixPart = false; + this.crontab = null; + } - public MainForm() - { - InitializeComponent(); - } + // ReSharper disable once InconsistentNaming + void Timer_Tick(object sender, EventArgs args) + { + var changeLapse = DateTime.Now - this.lastChangeTime; - // ReSharper disable once InconsistentNaming - void CronBox_Changed(object sender, EventArgs args) - { - _lastChangeTime = DateTime.Now; - _dirty = true; - _isSixPart = false; - _crontab = null; - } + if (!this.dirty || changeLapse <= TimeSpan.FromMilliseconds(500)) + return; - // ReSharper disable once InconsistentNaming - void Timer_Tick(object sender, EventArgs args) - { - var changeLapse = DateTime.Now - _lastChangeTime; + this.dirty = false; + DoCrontabbing(); + } - if (!_dirty || changeLapse <= TimeSpan.FromMilliseconds(500)) - return; + void DoCrontabbing() + { + this.resultBox.Clear(); + this.errorProvider.SetError(this.cronBox, null); + this.statusBarPanel.Text = "Ready"; + this.moreButton.Enabled = false; - _dirty = false; - DoCrontabbing(); - } + const string defaultCustomFormat = "dd/MM/yyyy HH:mm"; - void DoCrontabbing() + if (this.crontab == null) { - _resultBox.Clear(); - _errorProvider.SetError(_cronBox, null); - _statusBarPanel.Text = "Ready"; - _moreButton.Enabled = false; - - if (_crontab == null) + try { - try - { - var expression = _cronBox.Text.Trim(); + var expression = this.cronBox.Text.Trim(); - if (expression.Length == 0) - return; + if (expression.Length == 0) + return; - _isSixPart = expression.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Length == 6; - _crontab = CrontabSchedule.Parse(expression, new CrontabSchedule.ParseOptions { IncludingSeconds = _isSixPart }); + this.isSixPart = expression.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Length == 6; + this.crontab = CrontabSchedule.Parse(expression, new CrontabSchedule.ParseOptions { IncludingSeconds = this.isSixPart }); - _totalOccurrenceCount = 0; + this.totalOccurrenceCount = 0; - _startTime = DateTime.ParseExact(_startTimePicker.Text, - _startTimePicker.CustomFormat, CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal) - (_isSixPart ? TimeSpan.FromSeconds(1): TimeSpan.FromMinutes(1)); - } - catch (CrontabException e) - { - _errorProvider.SetError(_cronBox, e.Message); - - var traceBuilder = new StringBuilder(); + this.startTime = DateTime.ParseExact(this.startTimePicker.Text, + this.startTimePicker.CustomFormat ?? defaultCustomFormat, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal) - (this.isSixPart ? TimeSpan.FromSeconds(1): TimeSpan.FromMinutes(1)); + } + catch (CrontabException e) + { + this.errorProvider.SetError(this.cronBox, e.Message); - Exception traceException = e; - Exception lastException; + var traceBuilder = new StringBuilder(); - do - { - traceBuilder.Append(traceException.Message); - traceBuilder.Append("\r\n"); - lastException = traceException; - traceException = traceException.GetBaseException(); - } - while (lastException != traceException); + Exception traceException = e; + Exception lastException; - _resultBox.Text = traceBuilder.ToString(); - return; + do + { + _ = traceBuilder.Append(traceException.Message) + .Append("\r\n"); + lastException = traceException; + traceException = traceException.GetBaseException(); } + while (lastException != traceException); + this.resultBox.Text = traceBuilder.ToString(); + return; } - var endTime = DateTime.ParseExact(_endTimePicker.Text, - _endTimePicker.CustomFormat, CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal); - - var sb = new StringBuilder(); + } - var count = 0; - const int maxCount = 500; - var info = DateTimeFormatInfo.CurrentInfo; - var dayWidth = info.AbbreviatedDayNames.Max(s => s.Length); - var monthWidth = info.AbbreviatedMonthNames.Max(s => s.Length); - var timeComponent = _isSixPart ? "HH:mm:ss" : "HH:mm"; - var timeFormat = $"{{0,-{dayWidth}:ddd}} {{0:dd}}, {{0,-{monthWidth}:MMM}} {{0:yyyy {timeComponent}}}"; - var lastTimeString = new string('?', string.Format(timeFormat, DateTime.MinValue).Length); + var endTime = DateTime.ParseExact(this.endTimePicker.Text, + this.endTimePicker.CustomFormat ?? defaultCustomFormat, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal); - foreach (var occurrence in _crontab.GetNextOccurrences(_startTime, endTime)) - { - if (count + 1 > maxCount) - break; - - _startTime = occurrence; - _totalOccurrenceCount++; - count++; - - var timeString = string.Format(timeFormat, occurrence); - - sb.Append(timeString); - sb.Append(" | "); - - var index = Diff(lastTimeString, timeString, 0, dayWidth, sb); - sb.Append(' '); - index = Diff(lastTimeString, timeString, index + 1, 2, sb); - sb.Append(", "); - index = Diff(lastTimeString, timeString, index + 2, monthWidth, sb); - sb.Append(' '); - index = Diff(lastTimeString, timeString, index + 1, 4, sb); - sb.Append(' '); - index = Diff(lastTimeString, timeString, index + 1, 2, sb); - sb.Append(':'); - index = Diff(lastTimeString, timeString, index + 1, 2, sb); - if (_isSixPart) - { - sb.Append(':'); - Diff(lastTimeString, timeString, index + 1, 2, sb); - } + var sb = new StringBuilder(); - lastTimeString = timeString; + var count = 0; + const int maxCount = 500; + var info = DateTimeFormatInfo.CurrentInfo; + var dayWidth = info.AbbreviatedDayNames.Max(s => s.Length); + var monthWidth = info.AbbreviatedMonthNames.Max(s => s.Length); + var timeComponent = this.isSixPart ? "HH:mm:ss" : "HH:mm"; + var timeFormat = $"{{0,-{dayWidth}:ddd}} {{0:dd}}, {{0,-{monthWidth}:MMM}} {{0:yyyy {timeComponent}}}"; + var lastTimeString = new string('?', string.Format(null, timeFormat, DateTime.MinValue).Length); - sb.Append("\r\n"); + foreach (var occurrence in this.crontab.GetNextOccurrences(this.startTime, endTime)) + { + if (count + 1 > maxCount) + break; + + this.startTime = occurrence; + this.totalOccurrenceCount++; + count++; + + var timeString = string.Format(null, timeFormat, occurrence); + + _ = sb.Append(timeString) + .Append(" | "); + + var index = Diff(lastTimeString, timeString, 0, dayWidth, sb); + _ = sb.Append(' '); + index = Diff(lastTimeString, timeString, index + 1, 2, sb); + _ = sb.Append(", "); + index = Diff(lastTimeString, timeString, index + 2, monthWidth, sb); + _ = sb.Append(' '); + index = Diff(lastTimeString, timeString, index + 1, 4, sb); + _ = sb.Append(' '); + index = Diff(lastTimeString, timeString, index + 1, 2, sb); + _ = sb.Append(':'); + index = Diff(lastTimeString, timeString, index + 1, 2, sb); + if (this.isSixPart) + { + _ = sb.Append(':'); + _ = Diff(lastTimeString, timeString, index + 1, 2, sb); } - _moreButton.Enabled = count == maxCount; + lastTimeString = timeString; - _statusBarPanel.Text = $"Last count = {count:N0}, Total = {_totalOccurrenceCount:N0}"; - - _resultBox.Text = sb.ToString(); - _resultBox.Select(0, 0); - _resultBox.ScrollToCaret(); + _ = sb.Append("\r\n"); } - static int Diff(string oldString, string newString, int index, int length, StringBuilder builder) - { - if (string.CompareOrdinal(oldString, index, newString, index, length) == 0) - builder.Append('-', length); - else - builder.Append(newString, index, length); + this.moreButton.Enabled = count == maxCount; - return index + length; - } + this.statusBarPanel.Text = $"Last count = {count:N0}, Total = {this.totalOccurrenceCount:N0}"; - // ReSharper disable once InconsistentNaming - void More_Click(object sender, EventArgs e) => DoCrontabbing(); + this.resultBox.Text = sb.ToString(); + this.resultBox.Select(0, 0); + this.resultBox.ScrollToCaret(); } + + static int Diff(string oldString, string newString, int index, int length, StringBuilder builder) + { +#pragma warning disable IDE0045 // Convert to conditional expression (has side-effects) + if (string.CompareOrdinal(oldString, index, newString, index, length) == 0) + _ = builder.Append('-', length); + else + _ = builder.Append(newString, index, length); +#pragma warning restore IDE0045 // Convert to conditional expression + + return index + length; + } + + // ReSharper disable once InconsistentNaming + void More_Click(object sender, EventArgs e) => DoCrontabbing(); } diff --git a/NCrontabViewer/MainForm.resx b/NCrontabViewer/MainForm.resx index a32fc35..da6380f 100644 --- a/NCrontabViewer/MainForm.resx +++ b/NCrontabViewer/MainForm.resx @@ -117,10 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + 166, 17 - + 17, 17 \ No newline at end of file diff --git a/NCrontabViewer/NCrontabViewer.csproj b/NCrontabViewer/NCrontabViewer.csproj index 33bc6af..f244c5d 100644 --- a/NCrontabViewer/NCrontabViewer.csproj +++ b/NCrontabViewer/NCrontabViewer.csproj @@ -1,112 +1,35 @@ - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {02F42DAC-8A9F-45BB-B734-BB08F0D194C4} - WinExe - Properties - NCrontabViewer - NCrontabViewer - v3.5 - 512 - - - 3.5 - - Client - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - AllRules.ruleset - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - AllRules.ruleset - + + - - - + + - - CrontabException.cs - - - CrontabField.cs - - - CrontabFieldImpl.cs - - - CrontabFieldKind.cs - - - CrontabSchedule.cs - - - ErrorHandling.cs - - - ICrontabField.cs - - - StringSeparatorStock.cs - - - SolutionInfo.cs - - - - True - True - AssemblyInfo.g.tt - - - Form - - - MainForm.cs - - - - MainForm.cs - Designer - - - - - + TextTemplatingFileGenerator AssemblyInfo.g.cs + - + + - + + True + True + AssemblyInfo.g.tt + - - + + + WinExe + net8.0-windows + true + false + false + false + + \ No newline at end of file diff --git a/NCrontabViewer/Program.cs b/NCrontabViewer/Program.cs index 9456a99..16a09e2 100644 --- a/NCrontabViewer/Program.cs +++ b/NCrontabViewer/Program.cs @@ -17,23 +17,18 @@ // #endregion -namespace NCrontabViewer -{ - #region Imports - - using System; - using System.Windows.Forms; +using System; +using System.Windows.Forms; - #endregion +namespace NCrontabViewer; - static class Program +static class Program +{ + [STAThread] + static void Main() { - [STAThread] - static void Main() - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new MainForm()); - } + ApplicationConfiguration.Initialize(); + using var form = new MainForm(); + Application.Run(form); } } diff --git a/README.md b/README.md index ebbb073..ba2a4a0 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,20 @@ Star (`*`) in the value field above means all legal values as in parentheses for that column. The value column can have a `*` or a list of elements separated by commas. An element is either a number in the ranges shown above or two numbers in the range separated by a hyphen (meaning an inclusive range). For more, see -[CrontabExpression](https://github.com/atifaziz/NCrontab/wiki/Crontab-Expression). +[CrontabExpression]. + +The default format parsed by `CrontabSchedule.Parse` is the five-part cron +format. In order to use the six-part format that includes seconds, pass a +`CrontabSchedule.ParseOptions` to `Parse` with `IncludingSeconds` set to +`true`. For example: + +```csharp +var s = CrontabSchedule.Parse("0,30 * * * * *", + new CrontabSchedule.ParseOptions + { + IncludingSeconds = true + }); +``` Below is an example in [IronPython][ipy] of how to use `CrontabSchedule` class from NCrontab to generate occurrences of the schedule `0 12 * */2 Mon` @@ -149,7 +162,7 @@ Below is the same example in C# Interactive (`csi.exe`): > var start = new DateTime(2000, 1, 1); > var end = start.AddYears(1); > var occurrences = s.GetNextOccurrences(start, end); - > Console.WriteLine(string.Join(Environment.NewLine, + > Console.WriteLine(string.Join(Environment.NewLine, . from t in occurrences . select $"{t:ddd, dd MMM yyyy HH:mm}")); Mon, 03 Jan 2000 12:00 @@ -180,11 +193,154 @@ Below is the same example in C# Interactive (`csi.exe`): Mon, 20 Nov 2000 12:00 Mon, 27 Nov 2000 12:00 +Below is the same example in C# using [`dotnet-script`][dotnet-script]: + + > #r "nuget:NCrontab" + > using NCrontab; + > var s = CrontabSchedule.Parse("0 12 * */2 Mon"); + > var start = new DateTime(2000, 1, 1); + > var end = start.AddYears(1); + > var occurrences = s.GetNextOccurrences(start, end); + > Console.WriteLine(string.Join(Environment.NewLine, + * from t in occurrences + * select $"{t:ddd, dd MMM yyyy HH:mm}")); + Mon, 03 Jan 2000 12:00 + Mon, 10 Jan 2000 12:00 + Mon, 17 Jan 2000 12:00 + Mon, 24 Jan 2000 12:00 + Mon, 31 Jan 2000 12:00 + Mon, 06 Mar 2000 12:00 + Mon, 13 Mar 2000 12:00 + Mon, 20 Mar 2000 12:00 + Mon, 27 Mar 2000 12:00 + Mon, 01 May 2000 12:00 + Mon, 08 May 2000 12:00 + Mon, 15 May 2000 12:00 + Mon, 22 May 2000 12:00 + Mon, 29 May 2000 12:00 + Mon, 03 Jul 2000 12:00 + Mon, 10 Jul 2000 12:00 + Mon, 17 Jul 2000 12:00 + Mon, 24 Jul 2000 12:00 + Mon, 31 Jul 2000 12:00 + Mon, 04 Sept 2000 12:00 + Mon, 11 Sept 2000 12:00 + Mon, 18 Sept 2000 12:00 + Mon, 25 Sept 2000 12:00 + Mon, 06 Nov 2000 12:00 + Mon, 13 Nov 2000 12:00 + Mon, 20 Nov 2000 12:00 + Mon, 27 Nov 2000 12:00 + +Some complex schedules cannot be expressed in a single crontab expression so +NCrontab can produce _distinct occurrences_ given a sequence of +`CrontabSchedule` instances. In the C# example below, two schedules are merged +to produce a single set of occurrences over a week. The first schedule occurs +every 6 hours on weekdays while the second occurs every 12 hours on weekends. + + Microsoft (R) Visual C# Interactive Compiler version 1.2.0.60317 + Copyright (C) Microsoft Corporation. All rights reserved. + + Type "#help" for more information. + > using NCrontab; + > var s1 = CrontabSchedule.Parse("0 */6 * * Mon-Fri"); + > var s2 = CrontabSchedule.Parse("0 */12 * * Sat,Sun"); + > var s = new[] { s1, s2 }; + > var start = new DateTime(2000, 1, 1); + > var end = start.AddDays(7); + > var occurrences = s.GetNextOccurrences(start, end); + > // `Sat, 01 Jan 2000 10:00` won't appear because `start` is exclusive + > Console.WriteLine(string.Join(Environment.NewLine, + . from t in occurrences + . select $"{t:ddd, dd MMM yyyy HH:mm}")); + Sat, 01 Jan 2000 12:00 + Sun, 02 Jan 2000 00:00 + Sun, 02 Jan 2000 12:00 + Mon, 03 Jan 2000 00:00 + Mon, 03 Jan 2000 06:00 + Mon, 03 Jan 2000 12:00 + Mon, 03 Jan 2000 18:00 + Tue, 04 Jan 2000 00:00 + Tue, 04 Jan 2000 06:00 + Tue, 04 Jan 2000 12:00 + Tue, 04 Jan 2000 18:00 + Wed, 05 Jan 2000 00:00 + Wed, 05 Jan 2000 06:00 + Wed, 05 Jan 2000 12:00 + Wed, 05 Jan 2000 18:00 + Thu, 06 Jan 2000 00:00 + Thu, 06 Jan 2000 06:00 + Thu, 06 Jan 2000 12:00 + Thu, 06 Jan 2000 18:00 + Fri, 07 Jan 2000 00:00 + Fri, 07 Jan 2000 06:00 + Fri, 07 Jan 2000 12:00 + Fri, 07 Jan 2000 18:00 + +If one or more schedules produce the same occurrence then only one of them +if returned. + +## Merging Schedules + +NCrontab can merge the timeline of one or more schedules. This can sometimes +come handy when it's impossible to express a schedule with a single crontab +expression like _every 6 hours from 9 AM to 5 PM, on weekdays, but at noon on +weekends_. By breaking it up into two schedules: + +- `0 12 * * Sat-Sun`: at noon on weekends +- `0 9-17/6 * * Mon-Fri`: every 6 hours from 9 AM to 5 PM on weekdays + +you can merge them to produce a single timeline: + +```csharp +using System; +using NCrontab; + +var start = new DateTime(2000, 1, 1); +var end = start.AddYears(1); +var schedules = new[] +{ + CrontabSchedule.Parse("0 12 * * Sat-Sun"), + CrontabSchedule.Parse("0 9-17/6 * * Mon-Fri") +}; +var occurrences = schedules.GetNextOccurrences(start, end); +Console.WriteLine(string.Join(Environment.NewLine, + from t in occurrences + select $"{t:ddd, dd MMM yyyy HH:mm}")); +``` + +The output from a run will: + + Sat, 01 Jan 2000 12:00 + Sun, 02 Jan 2000 12:00 + Mon, 03 Jan 2000 09:00 + Mon, 03 Jan 2000 12:00 + Mon, 03 Jan 2000 15:00 + Tue, 04 Jan 2000 09:00 + Tue, 04 Jan 2000 12:00 + Tue, 04 Jan 2000 15:00 + Wed, 05 Jan 2000 09:00 + Wed, 05 Jan 2000 12:00 + Wed, 05 Jan 2000 15:00 + Thu, 06 Jan 2000 09:00 + Thu, 06 Jan 2000 12:00 + Thu, 06 Jan 2000 15:00 + Fri, 07 Jan 2000 09:00 + Fri, 07 Jan 2000 12:00 + Fri, 07 Jan 2000 15:00 + Sat, 08 Jan 2000 12:00 + Sun, 09 Jan 2000 12:00 + ... + +If two or more schedules produce the same occurrence then only one of them +is returned. + --- -This product includes software developed by the OpenSymphony Group (http://www.opensymphony.com/). +This product includes software developed by the [OpenSymphony Group]. + [CrontabExpression]: https://github.com/atifaziz/NCrontab/wiki/Crontab-Expression [ipy]: http://en.wikipedia.org/wiki/IronPython [f#]: http://msdn.microsoft.com/en-us/fsharp/cc742182 [build-badge]: https://img.shields.io/appveyor/ci/raboof/ncrontab/master.svg @@ -192,3 +348,5 @@ This product includes software developed by the OpenSymphony Group (http://www.o [nuget-pkg]: https://www.nuget.org/packages/ncrontab [builds]: https://ci.appveyor.com/project/raboof/ncrontab [netstd]: https://docs.microsoft.com/en-us/dotnet/articles/standard/library + [dotnet-script]: https://github.com/dotnet-script/dotnet-script + [OpenSymphony Group]: http://www.opensymphony.com/ diff --git a/appveyor.yml b/appveyor.yml index 19c21e6..d434b87 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,34 +1,64 @@ version: '{build}' image: - - Visual Studio 2017 + - Visual Studio 2022 - Ubuntu branches: only: - - master + - master + except: + - /.+[\-.]wip$/ + - wip skip_commits: files: + - '.git*' - '*.md' - '*.txt' skip_tags: true +pull_requests: + do_not_increment_build_number: true +environment: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true +install: + - cmd: curl -OsSL https://dot.net/v1/dotnet-install.ps1 + - ps: if ($isWindows) { ./dotnet-install.ps1 -JsonFile global.json } + - ps: if ($isWindows) { ./dotnet-install.ps1 -Runtime dotnet -Version 6.0.16 } + - sh: curl -OsSL https://dot.net/v1/dotnet-install.sh + - sh: chmod +x dotnet-install.sh + - sh: ./dotnet-install.sh --jsonfile global.json + - sh: ./dotnet-install.sh --runtime dotnet --version 6.0.16 + - sh: export PATH="$HOME/.dotnet:$PATH" before_build: - dotnet --info build_script: -- ps: >- - $id = $env:APPVEYOR_REPO_COMMIT_TIMESTAMP -replace '([-:]|\.0+Z)', '' - - $id = $id.Substring(0, 13) - +- ps: |- + $id = ([datetimeoffset]$env:APPVEYOR_REPO_COMMIT_TIMESTAMP).ToUniversalTime().ToString('yyyyMMdd''t''HHmm') if ($isWindows) { .\pack.cmd ci-$id } +after_build: +- ps: | + if ($isWindows) { + dotnet tool restore + dir dist\*.nupkg | % { + dotnet meziantou.validate-nuget-package --excluded-rules IconMustBeSet $_ + if ($LASTEXITCODE) { + throw "Package validation failed: $_" + } + } + } test_script: -- cmd: test.cmd +- cmd: | + call test.cmd + curl -OsSL https://uploader.codecov.io/latest/windows/codecov.exe + codecov - sh: ./test.sh for: - matrix: only: - - image: Visual Studio 2017 + - image: Visual Studio 2022 artifacts: - path: dist\*.nupkg + - path: etc\coverage deploy: - provider: NuGet server: https://www.myget.org/F/raboof/api/v2/package @@ -43,4 +73,4 @@ notifications: - ncrontab-builds@googlegroups.com on_build_success: true on_build_failure: true - on_build_status_changed: false \ No newline at end of file + on_build_status_changed: false diff --git a/build.cmd b/build.cmd index 6400277..d26c59d 100644 --- a/build.cmd +++ b/build.cmd @@ -1,36 +1,11 @@ @echo off pushd "%~dp0" -call :main +call :main %* popd goto :EOF :main -setlocal -for %%c in (Debug Release) do ( - call msbuild /p:Configuration=%%c /v:m NCrontabViewer\NCrontabViewer.csproj || exit /b 1 -) -:buildlib -set DOTNETEXE= -for %%f in (dotnet.exe) do set DOTNETEXE=%%~dpnx$PATH:f -if not defined DOTNETEXE set DOTNETEXE=%ProgramFiles%\dotnet -if not exist "%DOTNETEXE%" ( - echo .NET Core does not appear to be installed on this machine, which is - echo required to build the solution. You can install it from the URL below - echo and then try building again: - echo https://dot.net - exit /b 1 -) -"%DOTNETEXE%" restore ^ - && call :build NCrontab Debug ^ - && call :build NCrontab Release ^ - && call :build NCrontab.Signed Debug ^ - && call :build NCrontab.Signed Release ^ - && call :build NCrontab.Tests Debug ^ - && call :build NCrontab.Tests Release ^ - && call :build NCrontabConsole Debug ^ - && call :build NCrontabConsole Release -goto :EOF - -:build -"%DOTNETEXE%" build -c %2 %1 + dotnet restore ^ + && dotnet build --no-restore -c Debug ^ + && dotnet build --no-restore -c Release goto :EOF diff --git a/build.sh b/build.sh index 42026b3..f53bede 100755 --- a/build.sh +++ b/build.sh @@ -1,15 +1,8 @@ #!/usr/bin/env bash set -e -which dotnet 2>/dev/null || { - echo>&2 .NET Core does not appear to be installed on this machine, which is - echo>&2 required to build the solution. You can install it from the URL below - echo>&2 and then try building again: - echo>&2 https://dot.net - exit 1 -} cd "$(dirname "$0")" -dotnet restore for p in NCrontab NCrontab.Signed; do { + dotnet restore $p for c in Debug Release; do { for f in netstandard1.0 netstandard2.0; do { dotnet build --no-restore -c $c -f $f $p @@ -20,8 +13,13 @@ for p in NCrontab NCrontab.Signed; do { } done for p in NCrontabConsole NCrontab.Tests; do { - for c in Debug Release; do { - dotnet build --no-restore -c $c -f netcoreapp1.0 $p + dotnet restore $p +} +done +for c in Debug Release; do { + dotnet build --no-restore -c $c NCrontabConsole + for f in net8.0 net6.0; do { + dotnet build --no-restore -c $c -f $f NCrontab.Tests } done } diff --git a/global.json b/global.json index 21e27ce..e972eb1 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "2.1.500" + "version": "8.0.300", + "rollForward": "latestFeature" } -} \ No newline at end of file +} diff --git a/msbuild.cmd b/msbuild.cmd deleted file mode 100644 index 3763315..0000000 --- a/msbuild.cmd +++ /dev/null @@ -1,35 +0,0 @@ -@echo off -setlocal -if "%PROCESSOR_ARCHITECTURE%"=="x86" set PROGRAMS=%ProgramFiles% -if defined ProgramFiles(x86) set PROGRAMS=%ProgramFiles(x86)% -for %%e in (Community Professional Enterprise) do ( - if exist "%PROGRAMS%\Microsoft Visual Studio\2017\%%e\MSBuild\15.0\Bin\MSBuild.exe" ( - set "MSBUILD=%PROGRAMS%\Microsoft Visual Studio\2017\%%e\MSBuild\15.0\Bin\MSBuild.exe" - ) -) -if exist "%MSBUILD%" goto :build -set MSBUILD= -for %%i in (MSBuild.exe) do set MSBUILD=%%~dpnx$PATH:i -if not defined MSBUILD goto :nomsbuild -set MSBUILD_VERSION_MAJOR= -set MSBUILD_VERSION_MINOR= -for /f "delims=. tokens=1,2,3,4" %%m in ('msbuild /version /nologo') do ( - set MSBUILD_VERSION_MAJOR=%%m - set MSBUILD_VERSION_MINOR=%%n -) -if not defined MSBUILD_VERSION_MAJOR goto :nomsbuild -if not defined MSBUILD_VERSION_MINOR goto :nomsbuild -if %MSBUILD_VERSION_MAJOR% lss 15 goto :nomsbuild -if %MSBUILD_VERSION_MINOR% lss 1 goto :nomsbuild -:build -"%MSBUILD%" %* -goto :EOF - -:nomsbuild -echo>&2 Microsoft Build Engine 15.1 is required to build the solution. For -echo>&2 installation instructions, see: -echo>&2 https://docs.microsoft.com/en-us/visualstudio/install/use-command-line-parameters-to-install-visual-studio -echo>&2 At the very least, you will want to install the MSBuilt Tool workload -echo>&2 that has the identifier "Microsoft.VisualStudio.Workload.MSBuildTools": -echo>&2 https://docs.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools#msbuild-tools -exit /b s diff --git a/pack.cmd b/pack.cmd index 952e1a0..5c07c5d 100644 --- a/pack.cmd +++ b/pack.cmd @@ -6,16 +6,6 @@ goto :EOF :main setlocal -set DOTNETEXE= -for %%f in (dotnet.exe) do set DOTNETEXE=%%~dpnx$PATH:f -if not defined DOTNETEXE set DOTNETEXE=%ProgramFiles%\dotnet -if not exist "%DOTNETEXE%" ( - echo .NET Core does not appear to be installed on this machine, which is - echo required to build the solution. You can install it from the URL below - echo and then try building again: - echo https://dot.net - exit /b 1 -) set VERSION_SUFFIX= if not "%~1"=="" set VERSION_SUFFIX=--version-suffix %1 call build && call :pack NCrontab && call :pack NCrontab.Signed @@ -23,5 +13,5 @@ goto :EOF :pack setlocal -dotnet pack --no-restore --no-build -c Release %VERSION_SUFFIX% %1 +dotnet pack --no-build -c Release %VERSION_SUFFIX% %1 goto :EOF diff --git a/test.cmd b/test.cmd index 9236a82..32d54ae 100644 --- a/test.cmd +++ b/test.cmd @@ -1,11 +1,13 @@ @echo off pushd "%~dp0" -call build ^ - && call :test Debug ^ - && call :test Release ^ -popd -goto :EOF +dotnet tool restore ^ + && call build ^ + && call :test Debug ^ + && call :test Release ^ + && dotnet reportgenerator -reports:NCrontab.Tests\TestResults\*\coverage.cobertura.xml -targetdir:etc\coverage -reporttypes:TextSummary;Html ^ + && type etc\coverage\Summary.txt +popd && exit /b %ERRORLEVEL% :test -dotnet test --no-restore --no-build -c %1 NCrontab.Tests +dotnet test --no-build -s NCrontab.Tests\.runsettings -c %* goto :EOF diff --git a/test.sh b/test.sh index 4a0f4aa..3eefe55 100755 --- a/test.sh +++ b/test.sh @@ -1,9 +1,15 @@ #!/usr/bin/env bash set -e cd "$(dirname "$0")" -VERSION_SUFFIX= -if [ ! -z "$1" ]; then VERSION_SUFFIX="--version-suffix $1"; fi +dotnet tool restore ./build.sh -for c in Debug Release; do - dotnet test --no-restore --no-build -f netcoreapp1.0 -c $c NCrontab.Tests +for f in net8.0 net6.0; do + for c in Debug Release; do + dotnet test --no-build -c $c -f $f \ + -s NCrontab.Tests/.runsettings + done done +dotnet reportgenerator '-reports:NCrontab.Tests/TestResults/*/coverage.cobertura.xml' \ + -targetdir:etc/coverage \ + '-reporttypes:TextSummary;Html' +cat etc/coverage/Summary.txt