FixAntenna/NetCore/FixEngine/Scheduler/MultipartCronExpression.cs (90 lines of code) (raw):

// Copyright (c) 2022 EPAM Systems // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Linq; using Quartz; namespace Epam.FixAntenna.NetCore.FixEngine.Scheduler { /// <summary> /// Keeps a list of cron expressions. /// Cron expressions are passed in a string with the "|" delimiter. /// </summary> internal class MultipartCronExpression { private readonly CronExpression[] _cronExpressions; public string OriginalCronExpression { get; } public MultipartCronExpression(string pipedCronExpression, TimeZoneInfo timeZone) { OriginalCronExpression = pipedCronExpression ?? throw new ArgumentNullException(nameof(pipedCronExpression)); _cronExpressions = ExtractCronExpressions(pipedCronExpression) .Select(p => new CronExpression(p) { TimeZone = timeZone }) .ToArray(); } public static bool IsValidCronExpression(string pipedCronExpression) { var parts = ExtractCronExpressions(pipedCronExpression); return parts.All(CronExpression.IsValidExpression); } public bool IsSatisfiedBy(DateTimeOffset date) { return _cronExpressions.Any(p => p.IsSatisfiedBy(date)); } public static string[] ExtractCronExpressions(string pipedCronExpression) { return pipedCronExpression.Split('|'); } /// <summary> /// Find the closest time before the passed date that satisfied at least one of the cron expressions. /// See <see cref="GetTimeBefore(DateTimeOffset, CronExpression)"/> /// </summary> public DateTimeOffset? GetTimeBefore(DateTimeOffset date) { return _cronExpressions.Select(exp => GetTimeBefore(date, exp)).Max(); } /// <summary> /// Find the closest time after the passed date that satisfied at least one of the cron expressions. /// </summary> public DateTimeOffset? GetTimeAfter(DateTimeOffset date) { return _cronExpressions.Select(exp => exp.GetTimeAfter(date)).Min(); } /// <summary> /// Find the closest time before the passed date that satisfied the cron expression. /// Had to implement this as GetTimeBefore from CronExpression is not implemented and always returns null. /// /// Calculation is based on the existing GetNextValidTimeAfter method and the binary search idea. /// Basically, we need to find an appropriate value of the function y(t) = GetNextValidTimeAfter(t). /// This function is non-decreasing, and this fact allows using the binary search. /// We want to find "t" that satisfies the cron expression and y(t) >= date or y(t) is null. /// y(t) = null means that no time in future after the "t" value satisfies the cron expression). /// </summary> private static DateTimeOffset? GetTimeBefore(DateTimeOffset date, CronExpression exp) { date = RemoveMilliseconds(date); var low = DateTimeOffset.FromUnixTimeSeconds(0); var up = date; var second = TimeSpan.FromSeconds(1); // no previous date satisfies cron expression if (exp.GetNextValidTimeAfter(low) >= date) return null; while (low < up - second) { var mean = low + Divide(up - low, 2); mean = RemoveMilliseconds(mean); var nextValidTime = exp.GetNextValidTimeAfter(mean); if (exp.IsSatisfiedBy(mean) && (nextValidTime >= date || nextValidTime == null)) { return mean; } if (nextValidTime >= date || nextValidTime == null) { up = mean; } else { low = mean; } } throw new Exception($"Cannot find previous valid time for the cron expression: {exp}. {nameof(date)}: {date}"); } private static DateTimeOffset RemoveMilliseconds(DateTimeOffset offset) { return new DateTimeOffset(offset.Year, offset.Month, offset.Day, offset.Hour, offset.Minute, offset.Second, offset.Offset); } // Based on implementation in .NET 6. // Net standard 2.0 does not support division of TimeSpans. private static TimeSpan Divide(TimeSpan timeSpan, double divisor) { if (double.IsNaN(divisor)) { throw new ArgumentException("Argument cannot be NaN", nameof(divisor)); } double ticks = Math.Round(timeSpan.Ticks / divisor); return IntervalFromDoubleTicks(ticks); } // Based on implementation in .NET 6 private static TimeSpan IntervalFromDoubleTicks(double ticks) { if ((ticks > long.MaxValue) || (ticks < long.MinValue) || double.IsNaN(ticks)) { throw new OverflowException("TimeSpan too long"); } if (ticks == long.MaxValue) return TimeSpan.MaxValue; return new TimeSpan((long)ticks); } } }