-
-
Save vlova/544d693cc4083caafa477383b2e1c216 to your computer and use it in GitHub Desktop.
CronExpression parsing via regex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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