Skip to content

Instantly share code, notes, and snippets.

@vlova
Created August 10, 2021 10:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vlova/27c1eecdc17c139e33db6e2d78dcea2d to your computer and use it in GitHub Desktop.
Save vlova/27c1eecdc17c139e33db6e2d78dcea2d to your computer and use it in GitHub Desktop.
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