Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CronCraft.Benchmarks/CronCraft.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
Comment on lines +3 to +9

<ItemGroup>
<ProjectReference Include="..\CronCraft\CronCraft.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
</ItemGroup>

</Project>
65 changes: 65 additions & 0 deletions CronCraft.Benchmarks/CronHelperBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using CronCraft.Extensions;
using CronCraft.Models;

BenchmarkRunner.Run<CronHelperBenchmarks>();

[MemoryDiagnoser]
[ShortRunJob]
public class CronHelperBenchmarks
{
private CronSettings _settings = null!;
private CronSettings _settingsWithTz = null!;
private CronSettings _spanishSettings = null!;
private CronSettings _frenchSettings = null!;
private TimeZoneInfo _timeZone = null!;

[GlobalSetup]
public void Setup()
{
_settings = new CronSettings { Language = "en", DayNameFormat = "short" };

_timeZone = TimeZoneInfo.FindSystemTimeZoneById("W. Central Africa Standard Time");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f "CronHelperBenchmarks.cs" --exec cat -n {}

Repository: teesofttech/CronCraft

Length of output: 2834


🌐 Web query:

.NET TimeZoneInfo FindSystemTimeZoneById Windows timezone ID non-Windows platforms cross-platform IANA

💡 Result:

In.NET 6 and later, the TimeZoneInfo.FindSystemTimeZoneById method provides cross-platform support for both Windows time zone IDs (e.g., "Pacific Standard Time") and IANA time zone IDs (e.g., "America/Los_Angeles") regardless of the underlying operating system [1][2][3]. Key details regarding this functionality include: Integration and Requirements The runtime achieves this by leveraging the ICU (International Components for Unicode) library [1][4]. For this cross-platform resolution to function, the environment must have ICU support enabled [4]. On Windows, this requires that the application not be running in globalization invariant mode and that NLS (National Language Support) is not enabled [1][4]. On Linux-based environments (such as Docker containers), ensure the necessary time zone data (often provided by the tzdata package) is installed, as some minimal images may not include it by default [5]. Behavior When you call FindSystemTimeZoneById with an ID, the method attempts to resolve the identifier using the available system provider [1][3]. If the runtime detects an ID that does not match the native format of the host OS, it can automatically perform the necessary conversion to resolve the time zone correctly [5][3]. This eliminates the need for manual mapping or third-party conversion libraries in many modern applications [6][7]. Compatibility Note While.NET 6+ simplified this process, older versions of.NET (such as.NET Framework or early.NET Core versions) did not support cross-platform ID resolution [8][9]. In those environments, developers were required to either manually handle the differences using conditional logic or use community libraries like TimeZoneConverter to map between Windows and IANA identifiers [5][6][10]. For applications targeting.NET 6+, you can generally use either ID format with confidence, provided your environment is correctly configured with ICU support [1][4]. If you encounter issues in specific deployments, verifying that the host system has the required ICU and timezone data installed is the recommended first step [4][5].

Citations:


🏁 Script executed:

find . -name "*.csproj" -o -name "*.fsproj" | grep -i benchmark | head -5 | xargs cat

Repository: teesofttech/CronCraft

Length of output: 545


Add error handling for timezone resolution to ensure benchmark portability.

Line 23 uses a Windows timezone ID that requires proper environment configuration (ICU support and timezone data) to work cross-platform with .NET 8. While .NET 8 can resolve Windows IDs on non-Windows systems, environments without ICU or timezone data will fail with TimeZoneNotFoundException. Add a try-catch fallback or attempt alternative IDs to ensure the benchmark setup succeeds across all contributor environments.

Suggested patch
-        _timeZone = TimeZoneInfo.FindSystemTimeZoneById("W. Central Africa Standard Time");
+        _timeZone = TryResolveTimeZone("W. Central Africa Standard Time", "Africa/Lagos");
private static TimeZoneInfo TryResolveTimeZone(params string[] ids)
{
    foreach (var id in ids)
    {
        try { return TimeZoneInfo.FindSystemTimeZoneById(id); }
        catch (TimeZoneNotFoundException) { }
        catch (InvalidTimeZoneException) { }
    }
    return TimeZoneInfo.Utc;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CronCraft.Benchmarks/CronHelperBenchmarks.cs` at line 23, The benchmark setup
uses TimeZoneInfo.FindSystemTimeZoneById with a Windows-specific timezone ID
that may fail on non-Windows systems without ICU support, causing
TimeZoneNotFoundException and breaking the benchmark on contributor machines.
Create a helper method (e.g., TryResolveTimeZone) that attempts to resolve the
timezone ID but includes try-catch error handling for TimeZoneNotFoundException
and InvalidTimeZoneException, with a fallback to TimeZoneInfo.Utc if resolution
fails. Then replace the direct FindSystemTimeZoneById call with a call to this
new helper method to ensure the benchmark setup succeeds across all
environments.

_settingsWithTz = new CronSettings { Language = "en", DayNameFormat = "short" };

_spanishSettings = new CronSettings { Language = "es", DayNameFormat = "short" };
_frenchSettings = new CronSettings { Language = "fr", DayNameFormat = "short" };
}

[Benchmark(Description = "*/5 * * * *")]
public string Simple_EveryFiveMinutes() =>
"*/5 * * * *".ToHumanReadable(_settings);

[Benchmark(Description = "0 */2 * * 1,2,3,4,5")]
public string WithDayList_Weekdays() =>
"0 */2 * * 1,2,3,4,5".ToHumanReadable(_settings);

[Benchmark(Description = "0 4 27 */6 ?")]
public string WithDayOfMonth() =>
"0 4 27 */6 ?".ToHumanReadable(_settings);

[Benchmark(Description = "0 15 10 * * ? (Quartz 6-part)")]
public string Quartz6Part() =>
"0 15 10 * * ?".ToHumanReadable(_settings);

[Benchmark(Description = "0 0 12 1/1 * ? * (Quartz 7-part)")]
public string Quartz7Part() =>
"0 0 12 1/1 * ? *".ToHumanReadable(_settings);

[Benchmark(Description = "30 14 * * * (with timezone)")]
public string WithTimezone() =>
"30 14 * * *".ToHumanReadable(_settingsWithTz, _timeZone);

[Benchmark(Description = "0 9 * * 1 (Spanish)")]
public string Spanish() =>
"0 9 * * 1".ToHumanReadable(_spanishSettings);

[Benchmark(Description = "0 9 * * 1 (French)")]
public string French() =>
"0 9 * * 1".ToHumanReadable(_frenchSettings);

[Benchmark(Description = "0 23 15 * 1 (day + weekday)")]
public string DayOfMonthAndWeekday() =>
"0 23 15 * 1".ToHumanReadable(_settings);
}
42 changes: 42 additions & 0 deletions CronCraft.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,66 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CronCraft.Test", "CronCraft
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CronCraft.Example", "CronCraft.Example\CronCraft.Example.csproj", "{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CronCraft.Benchmarks", "CronCraft.Benchmarks\CronCraft.Benchmarks.csproj", "{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
Comment on lines 15 to +21
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Debug|x64.ActiveCfg = Debug|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Debug|x64.Build.0 = Debug|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Debug|x86.ActiveCfg = Debug|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Debug|x86.Build.0 = Debug|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Release|Any CPU.Build.0 = Release|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Release|x64.ActiveCfg = Release|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Release|x64.Build.0 = Release|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Release|x86.ActiveCfg = Release|Any CPU
{B0387EA9-0BBD-48E5-AE59-862A0C59643D}.Release|x86.Build.0 = Release|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Debug|x64.ActiveCfg = Debug|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Debug|x64.Build.0 = Debug|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Debug|x86.ActiveCfg = Debug|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Debug|x86.Build.0 = Debug|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Release|Any CPU.Build.0 = Release|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Release|x64.ActiveCfg = Release|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Release|x64.Build.0 = Release|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Release|x86.ActiveCfg = Release|Any CPU
{D752194A-454F-447E-8B2D-3FCFB32208BD}.Release|x86.Build.0 = Release|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Debug|x64.ActiveCfg = Debug|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Debug|x64.Build.0 = Debug|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Debug|x86.ActiveCfg = Debug|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Debug|x86.Build.0 = Debug|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Release|Any CPU.Build.0 = Release|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Release|x64.ActiveCfg = Release|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Release|x64.Build.0 = Release|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Release|x86.ActiveCfg = Release|Any CPU
{ED0810B0-54BD-48E9-94FD-B7699D7CBD9B}.Release|x86.Build.0 = Release|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Debug|x64.ActiveCfg = Debug|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Debug|x64.Build.0 = Debug|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Debug|x86.ActiveCfg = Debug|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Debug|x86.Build.0 = Debug|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Release|Any CPU.Build.0 = Release|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Release|x64.ActiveCfg = Release|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Release|x64.Build.0 = Release|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Release|x86.ActiveCfg = Release|Any CPU
{FCC4D803-0AA1-43D7-9C67-4FC0CE365759}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,31 @@ Press Enter to exit...

---

## 📊 Performance Benchmarks

CronCraft includes a [BenchmarkDotNet](https://benchmarkdotnet.org/) project to measure parsing performance across expression types and languages.

**Run the benchmarks:**

```bash
cd CronCraft.Benchmarks
dotnet run -c Release -- --filter *
```
Comment on lines +114 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quote benchmark filter wildcards to avoid shell glob expansion.

The commands on Line 114 and Line 127 use unquoted *; shells may expand these before dotnet receives them, causing unexpected filtering behavior.

Suggested patch
-dotnet run -c Release -- --filter *
+dotnet run -c Release -- --filter "*"
...
-> Tip: add `--filter *Simple*` to run a single benchmark by name.
+> Tip: add `--filter "*Simple*"` to run a single benchmark by name.

Also applies to: 127-127

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 114 - 115, The unquoted wildcard `*` in the dotnet
filter commands on lines 114 and 127 will be expanded by the shell before dotnet
receives them, causing unexpected filtering behavior. Quote the wildcard filter
argument in both locations to prevent shell glob expansion: change `--filter *`
to `--filter "*"` on line 114 and similarly on line 127 so the literal asterisk
is passed to dotnet as intended.


**Sample results** (Apple M-series, .NET 8):

| Benchmark | Mean | Allocated |
|-----------|------|-----------|
| Simple (`*/5 * * * *`) | ~1 µs | minimal |
| With day list (`0 */2 * * 1,2,3,4,5`) | ~2 µs | minimal |
| Quartz 6-part | ~1.5 µs | minimal |
| With timezone | ~2 µs | minimal |
| Spanish | ~1.5 µs | minimal |

> Tip: add `--filter *Simple*` to run a single benchmark by name.

---

## 🤝 Contributing

Contributions are welcome! If you'd like to:
Expand Down
Loading