-
-
Save vlova/27c1eecdc17c139e33db6e2d78dcea2d to your computer and use it in GitHub Desktop.
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; | |
namespace Cron | |
{ | |
public class Program | |
{ | |
public static void Main() | |
{ | |
TestParsing(); | |
} | |
private static void TestParsing() | |
{ | |
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(CronExpressionParser.ParseSchedule(sample)?.ToString() ?? "Failed to parse"); | |
} | |
} | |
} | |
public static class CronExpressionParser | |
{ | |
public static ParsedSchedule ParseSchedule(string schedule) | |
{ | |
var items = schedule.Split(" "); | |
var parsedSchedule = (TryGet(items, 0), TryGet(items, 1), TryGet(items, 2)) switch | |
{ | |
(string time, null, null) | |
=> ParseSchedule(null, null, time), | |
(string date, string time, null) | |
=> ParseSchedule(date, null, time), | |
(string date, string dayOfWeek, string time) | |
=> ParseSchedule(date, dayOfWeek, time), | |
_ | |
=> null | |
}; | |
parsedSchedule.Source = schedule; | |
return parsedSchedule; | |
} | |
private static ParsedSchedule ParseSchedule(string date, string dayOfWeek, string time) | |
{ | |
var dateSchedule = ParseDate(date); | |
var dayOfWeekSchedule = ParseDayOfWeek(dayOfWeek); | |
var timeSchedule = ParseTimeWithMs(time); | |
if (dateSchedule == null || dayOfWeekSchedule == null || timeSchedule == null) | |
{ | |
throw new InvalidOperationException("Failed to parse"); | |
} | |
return new ParsedSchedule | |
{ | |
Year = dateSchedule.Year, | |
Month = dateSchedule.Month, | |
Day = dateSchedule.Day, | |
DayOfWeek = dayOfWeekSchedule.DayOfWeek, | |
Hour = timeSchedule.Hour, | |
Minutes = timeSchedule.Minutes, | |
Seconds = timeSchedule.Seconds, | |
Milliseconds = timeSchedule.Milliseconds, | |
}; | |
} | |
private static ParsedSchedule ParseDate(string date) | |
{ | |
var splitted = date == null | |
? new string[] { null, null, null } | |
: date.Split("."); | |
if (splitted.Length != 3) | |
{ | |
return null; | |
} | |
return new ParsedSchedule | |
{ | |
Year = ParseRange(ForceGet(splitted, 0), validRange: Range(2000, 2100)), | |
Month = ParseRange(ForceGet(splitted, 1), validRange: Range(1, 12)), | |
Day = ParseRange(ForceGet(splitted, 2), validRange: Range(1, 32), customIfEmpty: Range(1, 31)), | |
}; | |
} | |
private static ParsedSchedule ParseDayOfWeek(string dayOfWeek) | |
{ | |
return new ParsedSchedule | |
{ | |
DayOfWeek = ParseRange(dayOfWeek, validRange: Range(0, 6)) | |
}; | |
} | |
private static ParsedSchedule ParseTimeWithMs(string time) | |
{ | |
var splitted = time.Split("."); | |
return ParseTimeWithMs( | |
ForceGet(splitted, 0), | |
TryGet(splitted, 1)); | |
} | |
private static ParsedSchedule ParseTimeWithMs(string time, string milliseconds) | |
{ | |
var splitted = time.Split(":"); | |
return new ParsedSchedule | |
{ | |
Hour = ParseRange(ForceGet(splitted, 0), validRange: Range(0, 23)), | |
Minutes = ParseRange(ForceGet(splitted, 1), validRange: Range(0, 59)), | |
Seconds = ParseRange(ForceGet(splitted, 2), validRange: Range(0, 59)), | |
Milliseconds = ParseRange(milliseconds, validRange: Range(0, 1000), customIfEmpty: Range(0, 0)) | |
}; | |
} | |
private static CronScheduleRange ParseRange(string range, Range validRange, Range? customIfEmpty = null) | |
{ | |
if (range == null) | |
{ | |
return new CronScheduleRange((customIfEmpty ?? validRange).EnumerateRange()); | |
} | |
var subRanges = range.Split(","); | |
var totalRange = new CronScheduleRange(subRanges.SelectMany(ParseSubRange)); | |
var isTotalRangeValid = totalRange.All(item => (validRange.Start.Value <= item) && (item < validRange.End.Value)); | |
if (!isTotalRangeValid) | |
{ | |
throw new InvalidOperationException("Failed to parse"); | |
} | |
return totalRange; | |
CronScheduleRange ParseSubRange(string subRange) | |
{ | |
if (subRange == "*") | |
{ | |
return new CronScheduleRange(validRange.EnumerateRange()); | |
} | |
var splittedByStep = subRange.Split("/"); | |
var splittedByRange = ForceGet(splittedByStep, 0).Split("-"); | |
var fromStr = ForceGet(splittedByRange, 0); | |
var toStr = TryGet(splittedByRange, 1); | |
var stepStr = TryGet(splittedByStep, 1); | |
return (fromStr, toStr, stepStr) switch | |
{ | |
("*", null, null) | |
=> new CronScheduleRange(validRange.EnumerateRange()), | |
(string singleValue, null, null) | |
=> new CronScheduleRange { ParseNumber(singleValue) }, | |
("*", null, string step) | |
=> new CronScheduleRange(validRange.EnumerateRange(ParseNumber(step))), | |
(string from, string to, null) | |
=> new CronScheduleRange(Range(ParseNumber(from), ParseNumber(to)).EnumerateRange()), | |
(string from, string to, string step) | |
=> new CronScheduleRange(Range(ParseNumber(from), ParseNumber(to)).EnumerateRange(ParseNumber(step))), | |
_ => throw new InvalidOperationException("Failed to parse") | |
}; | |
} | |
} | |
private static string TryGet(string[] array, int index) | |
=> index < array.Length | |
? array[index] | |
: null; | |
private static string ForceGet(string[] array, int index) | |
=> index < array.Length | |
? array[index] | |
: throw new InvalidOperationException("Failed to parse"); | |
private static int ParseNumber(string str) | |
=> int.TryParse(str, out var number) | |
? number | |
: throw new InvalidOperationException("Failed to parse"); | |
private static Range Range(int from, int to) | |
{ | |
return from..(to + 1); | |
} | |
} | |
public class CronScheduleRange : SortedSet<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 record ParsedSchedule | |
{ | |
public string Source { get; set; } | |
public CronScheduleRange Year { get; set; } | |
public CronScheduleRange Month { get; set; } | |
public CronScheduleRange Day { get; set; } | |
public CronScheduleRange DayOfWeek { get; set; } | |
public CronScheduleRange Hour { get; set; } | |
public CronScheduleRange Minutes { get; set; } | |
public CronScheduleRange Seconds { get; set; } | |
public CronScheduleRange Milliseconds { get; set; } | |
} | |
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