Skip to content

Instantly share code, notes, and snippets.

@vlova
Created August 6, 2021 12:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vlova/544d693cc4083caafa477383b2e1c216 to your computer and use it in GitHub Desktop.
Save vlova/544d693cc4083caafa477383b2e1c216 to your computer and use it in GitHub Desktop.
CronExpression parsing via regex
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
public class Program
{
public static void Main()
{
var samples = new [] {
"2020.05.05 2 20:01:02.003",
"2020.05.05 20:01:02.003",
"20:01:02.003",
"2020.05.05 2 20:01:02",
"2020.05.05 20:01:02",
"20:01:02",
"20:01:02",
"1,2,3-5,10-20/3:00:00",
"*/4:00:00",
"**:01:02",
};
foreach (var sample in samples) {
Console.WriteLine(CronExpression.ParseSchedule(sample)?.ToString() ?? "Failed to parse");
}
}
}
public static class CronExpression {
public record ParsedSchedule {
public string Source { get; set; }
public CronScheduleRange Year { get; set; }
public CronScheduleRange Month { get; set; }
public CronScheduleRange Day { get; set; }
public CronScheduleRange Week { get; set; }
public CronScheduleRange Hour { get; set; }
public CronScheduleRange Minutes { get; set; }
public CronScheduleRange Seconds { get; set; }
public CronScheduleRange Milliseconds { get; set; }
}
public static ParsedSchedule ParseSchedule(string schedule) {
var partPattern = @"(?:\d|,|\-|\/|\*)+";
var pattern = $@"
(?:
(?<year>{partPattern})\.
(?<month>{partPattern})\.
(?<day>{partPattern})
(?:\s(?<week>{partPattern}))?
\s
)?
(?:
(?<hour>{partPattern}):
(?<minutes>{partPattern}):
(?<seconds>{partPattern})
(?:.(?<milliseconds>{partPattern}))?
)$
";
var matches = Regex.Matches(schedule, pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
if (matches.Count == 0) {
return null;
}
var match = matches[0];
Range Range(int from, int to) {
return from..(to+1);
}
var hasErrors = false;
// caveat: ParseGroup is modifying state (hasErrors)
CronScheduleRange ParseGroup(string name, Range validRange, Range? customIfEmpty = null) {
var group = match.Groups[name];
if (!group.Success) {
return new CronScheduleRange((customIfEmpty ?? validRange).EnumerateRange());
}
var subRanges = group.Value
.Split(",")
.Select(subList => ParseCronSubRange(subList, validRange))
.ToList();
if (subRanges.Any(subRange => subRange == null)) {
hasErrors = true;
return null;
}
var returnList = new CronScheduleRange(subRanges.SelectMany(x => x));
var isListValid = returnList.All(item => (validRange.Start.Value <= item) && (item < validRange.End.Value));
if (!isListValid) {
hasErrors = true;
return null;
}
return returnList;
}
var parsedSchedule = new ParsedSchedule {
Source = schedule,
Year = ParseGroup("year", validRange: Range(2000, 2100)),
Month = ParseGroup("month", validRange: Range(1, 12)),
Day = ParseGroup("day", validRange: Range(1, 32), customIfEmpty: Range(1, 31)),
Week = ParseGroup("week", validRange: Range(0, 6)),
Hour = ParseGroup("hour", validRange: Range(0, 23)),
Minutes = ParseGroup("minutes", validRange: Range(0, 59)),
Seconds = ParseGroup("seconds", validRange: Range(0, 59)),
Milliseconds = ParseGroup("milliseconds", validRange: Range(0, 999), customIfEmpty: Range(0,0)),
};
return hasErrors ? null : parsedSchedule;
}
private static CronScheduleRange ParseCronSubRange(string range, Range validRange) {
var cronPartPattern = @"
^(?:
(?:(?<from>\d+)-)?
(?<to>(?:(?:\d+)|\*))
(?:\/(?<step>\d+))?
)$
";
var matches = Regex.Matches(range, cronPartPattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
if (matches.Count == 0) {
return null;
}
var match = matches[0];
CronIndex ParseIndex(string groupName) {
var group = match.Groups[groupName];
if (!group.Success) {
return null;
}
if (group.Value == "*") {
return new CronAnyIndex();
}
return new CronNumericIndex(int.Parse(group.Value));
}
CronScheduleRange GenerateRange(CronIndex fromIdx, CronIndex toIdx, CronIndex stepIdx) {
return (fromIdx, toIdx, stepIdx) switch {
(null, CronNumericIndex(var to), null)
=> new CronScheduleRange { to },
(null, CronAnyIndex _, null)
=> new CronScheduleRange(validRange.EnumerateRange()),
(null, CronAnyIndex _, CronNumericIndex (var step))
=> new CronScheduleRange(validRange.EnumerateRange(step)),
(CronNumericIndex(var from), CronNumericIndex(var to), null)
=> new CronScheduleRange((from..(to+1)).EnumerateRange()),
(CronNumericIndex(var from), CronNumericIndex(var to), CronNumericIndex(var step))
=> new CronScheduleRange((from..(to+1)).EnumerateRange(step)),
(_, _, _) => null
};
}
return GenerateRange(
ParseIndex("from"),
ParseIndex("to"),
ParseIndex("step"));
}
private abstract record CronIndex() {}
private sealed record CronAnyIndex() : CronIndex() {}
private sealed record CronNumericIndex(int number) : CronIndex() {}
public class CronScheduleRange : List<int> {
public CronScheduleRange() : base() {}
public CronScheduleRange(IEnumerable<int> collection) : base(collection) {}
public override string ToString() {
if (!this.Any()) return "[]";
List<(int from, int to)> GetSubRanges() {
var subRanges = new List<(int from, int to)>();
int? startItem = null;
int? lastSeenItem = null;
foreach (var item in this) {
var isInRange = lastSeenItem == null || lastSeenItem == item - 1;
if (!isInRange) {
subRanges.Add((startItem.Value, lastSeenItem.Value));
startItem = item;
} else {
startItem ??= item;
}
lastSeenItem = item;
}
subRanges.Add((startItem.Value, lastSeenItem.Value));
return subRanges;
}
string FormatSubRange((int from, int to) subRange) {
if (subRange.from == subRange.to) {
return subRange.from.ToString();
}
return $"{subRange.from}-{subRange.to}";
}
return "[" + string.Join(",", GetSubRanges().Select(FormatSubRange)) + "]";
}
}
}
public static class Extensions {
public static IEnumerable<int> EnumerateRange(this Range range, int step = 1) {
if (range.Start.IsFromEnd || range.End.IsFromEnd) {
throw new NotImplementedException();
}
for (var i = range.Start.Value; i < range.End.Value; i += step) {
yield return i;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment