[fix]修正从池里借出来ISpanBuilder没有设置StartTime的问题
大石头 authored at 2024-11-15 16:58:54
11.02 KiB
X
namespace NewLife.Threading;

/// <summary>轻量级Cron表达式</summary>
/// <remarks>
/// 基本构成:秒+分+时+天+月+星期+年
/// 每段构成:
///     * 所有可能的值,该类型片段全部可选
///     , 列出枚举值
///     - 范围,横杠表示的一个区间可选
///     / 指定数值的增量,在上述可选数字内,间隔多少选一个
///     ? 不指定值,仅日期和星期域支持该字符
///     # 确定每个月第几个星期几,L表示倒数,仅星期域支持该字符
///     数字,具体某个数值可选
///     逗号多选,逗号分隔的多个数字或区间可选
/// </remarks>
/// <example>
/// */2 每两秒一次
/// 0,1,2 * * * * 每分钟的0秒1秒2秒各一次
/// 5/20 * * * * 每分钟的5秒25秒45秒各一次
/// * 1-10,13,25/3 * * * 每小时的1分4分7分10分13分25分,每一秒各一次
/// 0 0 0 1 * * 每个月1日的0点整
/// 0 0 2 * * 1-5 每个工作日的凌晨2点
/// 0 0 0 ? ? 1-7#1 每月第一周的任意一天(周一~周日)的0点整
/// 0 0 0 ? ? 3-5#L2 每个月倒数第二个星期三到星期五的0点整
/// 
/// 星期部分采用Linux和.NET风格,0表示周日,1表示周一。
/// 可设置Sunday为1,1表示周日,2表示周一。
/// 
/// 文档 https://newlifex.com/core/cron
/// 参考文档 https://help.aliyun.com/document_detail/64769.html
/// </example>
public class Cron
{
    #region 属性
    /// <summary>秒数集合</summary>
    public Int32[]? Seconds { get; set; }

    /// <summary>分钟集合</summary>
    public Int32[]? Minutes { get; set; }

    /// <summary>小时集合</summary>
    public Int32[]? Hours { get; set; }

    /// <summary>日期集合</summary>
    public Int32[]? DaysOfMonth { get; set; }

    /// <summary>月份集合</summary>
    public Int32[]? Months { get; set; }

    /// <summary>星期集合。key是星期数,value是第几个,负数表示倒数</summary>
    public IDictionary<Int32, Int32>? DaysOfWeek { get; set; }

    /// <summary>星期天偏移量。周日对应的数字,默认0。1表示周日时,2表示周一</summary>
    public Int32 Sunday { get; set; }

    private String? _expression;
    #endregion

    #region 构造
    /// <summary>实例化Cron表达式</summary>
    public Cron() { }

    /// <summary>实例化Cron表达式</summary>
    /// <param name="expression"></param>
    public Cron(String expression) => Parse(expression);

    /// <summary>已重载。</summary>
    /// <returns></returns>
    public override String ToString() => _expression ?? nameof(Cron);
    #endregion

    #region 方法
    /// <summary>指定时间是否位于表达式之内</summary>
    /// <param name="time"></param>
    /// <returns></returns>
    public Boolean IsTime(DateTime time)
    {
        if (Seconds == null || Minutes == null || Hours == null || DaysOfMonth == null || Months == null) return false;

        // 基础时间判断
        if (!Seconds.Contains(time.Second) ||
            !Minutes.Contains(time.Minute) ||
            !Hours.Contains(time.Hour) ||
            !DaysOfMonth.Contains(time.Day) ||
            !Months.Contains(time.Month)
            ) return false;

        var w = (Int32)time.DayOfWeek + Sunday;
        if (DaysOfWeek == null || !DaysOfWeek.TryGetValue(w, out var index)) return false;

        // 第几个星期几判断
        if (index > 0)
        {
            var start = new DateTime(time.Year, time.Month, 1);
            for (var dt = start; dt <= time.Date; dt = dt.AddDays(1))
            {
                if (dt.DayOfWeek == time.DayOfWeek) index--;
            }
            if (index != 0) return false;
        }
        else if (index < 0)
        {
            var start = new DateTime(time.Year, time.Month, 1);
            for (var dt = start.AddMonths(1).AddDays(-1); dt >= time.Date; dt = dt.AddDays(-1))
            {
                if (dt.DayOfWeek == time.DayOfWeek) index++;
            }
            if (index != 0) return false;
        }

        return true;
    }

    /// <summary>分析表达式</summary>
    /// <param name="expression"></param>
    /// <returns></returns>
    public Boolean Parse(String expression)
    {
        var ss = expression.Split([' '], StringSplitOptions.RemoveEmptyEntries);
        if (ss.Length == 0) return false;

        if (!TryParse(ss[0], 0, 60, out var vs)) return false;
        Seconds = vs;
        if (!TryParse(ss.Length > 1 ? ss[1] : "*", 0, 60, out vs)) return false;
        Minutes = vs;
        if (!TryParse(ss.Length > 2 ? ss[2] : "*", 0, 24, out vs)) return false;
        Hours = vs;
        if (!TryParse(ss.Length > 3 ? ss[3] : "*", 1, 32, out vs)) return false;
        DaysOfMonth = vs;
        if (!TryParse(ss.Length > 4 ? ss[4] : "*", 1, 13, out vs)) return false;
        Months = vs;

        var weeks = new Dictionary<Int32, Int32>();
        if (!TryParseWeek(ss.Length > 5 ? ss[5] : "*", 0, 7, weeks)) return false;
        DaysOfWeek = weeks;

        _expression = expression;

        return true;
    }

    private static Boolean TryParse(String value, Int32 start, Int32 max, out Int32[] vs)
    {
        // 固定值,最为常见,优先计算
        if (Int32.TryParse(value, out var n))
        {
            vs = [n];
            return true;
        }

        var rs = new List<Int32>();
        vs = new Int32[0];

        // 递归处理混合值
        if (value.Contains(','))
        {
            foreach (var item in value.Split(','))
            {
                if (!TryParse(item, start, max, out var arr)) return false;
                if (arr.Length > 0) rs.AddRange(arr);
            }
            vs = rs.ToArray();
            return true;
        }

        // 步进值
        var step = 1;
        var p = value.IndexOf('/');
        if (p > 0)
        {
            step = value[(p + 1)..].ToInt();
            value = value[..p];
        }

        // 连续范围
        var s = start;
        if (value is "*" or "?")
            s = 0;
        else if ((p = value.IndexOf('-')) > 0)
        {
            s = value[..p].ToInt();
            max = value[(p + 1)..].ToInt() + 1;
        }
        else if (Int32.TryParse(value, out n))
            s = n;
        else
            return false;

        for (var i = s; i < max; i += step)
        {
            if (i >= start) rs.Add(i);
        }

        vs = rs.ToArray();
        return true;
    }

    private static Boolean TryParseWeek(String value, Int32 start, Int32 max, IDictionary<Int32, Int32> weeks)
    {
        // 固定值,最为常见,优先计算
        if (Int32.TryParse(value, out var n))
        {
            weeks[n] = 0;
            return true;
        }

        // 递归处理混合值
        if (value.Contains(','))
        {
            foreach (var item in value.Split(','))
            {
                if (!TryParseWeek(item, start, max, weeks)) return false;
            }
            return true;
        }

        // 步进值
        var step = 1;
        var v = value;
        var p = value.IndexOf('/');
        if (p > 0)
        {
            step = value[(p + 1)..].ToInt();
            v = value[..p];
        }

        // 第几个星期几
        var index = 0;
        p = v.IndexOf('#');
        if (p > 0)
        {
            var str = v[(p + 1)..];
            if (str.StartsWithIgnoreCase("L"))
                index = -str[1..].ToInt();
            else
                index = str.ToInt();
            v = v[..p];
            step = 7;
        }

        // 连续范围
        var s = start;
        if (v is "*" or "?")
            s = 0;
        else if ((p = v.IndexOf('-')) > 0)
        {
            s = v[..p].ToInt();
            max = v[(p + 1)..].ToInt() + 1;
            step = 1;
        }
        else if (Int32.TryParse(v, out n))
            s = n;
        else
            return false;

        for (var i = s; i < max; i += step)
        {
            if (i >= start) weeks.Add(i, index);
        }

        return true;
    }

    /// <summary>获得指定时间之后的下一次执行时间,不含指定时间</summary>
    /// <remarks>
    /// 如果指定时间带有毫秒,则向前对齐。如09:14.123的"15 * * *"下一次是10:15而不是09:15
    /// </remarks>
    /// <param name="time">从该时间秒的下一秒算起的下一个执行时间</param>
    /// <returns>下一次执行时间(秒级),如果没有匹配则返回最小时间</returns>
    public DateTime GetNext(DateTime time)
    {
        // 如果指定时间带有毫秒,则向前对齐。如09:14.123格式化为09:15,计算下一次就从09:16开始
        var start = time.Trim();
        if (start != time)
            start = start.AddSeconds(2);
        else
            start = start.AddSeconds(1);

        // 设置末尾,避免死循环越界
        var end = time.AddYears(1);
        for (var dt = start; dt < end; dt = dt.AddSeconds(1))
        {
            if (IsTime(dt)) return dt;
        }

        return DateTime.MinValue;
    }

    /// <summary>获得与指定时间时间符合表达式的最远时间(秒级)</summary>
    /// <param name="time"></param>
    public DateTime GetPrevious(DateTime time)
    {
        // 如果指定时间带有毫秒,则向前对齐。如09:14.123格式化为09:15,计算下一次就从09:16开始
        var start = time.Trim();
        if (start != time)
            start = start.AddSeconds(-1);
        else
            start = start.AddSeconds(-2);

        // 设置末尾,避免死循环越界
        var end = time.AddYears(-1);
        var last = false;
        for (var dt = start; dt > end; dt = dt.AddSeconds(-1))//过去一年内
        {
            if (last == false)
            {
                last = IsTime(dt);//找真值
            }
            else
            {
                if (IsTime(dt) == false)//真值找到了找假值
                {
                    return dt.AddSeconds(1);//减多了,返回真值
                }
            }
            //if (last == true && IsTime(dt) == false) return dt.AddSeconds(1);
            //last = IsTime(dt);
        }

        return DateTime.MinValue;
    }

    /// <summary>对一批Cron表达式,获取下一次执行时间</summary>
    /// <param name="crons"></param>
    /// <param name="time"></param>
    /// <returns></returns>
    public static DateTime GetNext(String[] crons, DateTime time)
    {
        var next = DateTime.MaxValue;
        foreach (var item in crons)
        {
            var cron = new Cron(item);
            var dt = cron.GetNext(time);
            if (dt < next) next = dt;
        }
        return next;
    }

    /// <summary>对一批Cron表达式,获取前一次执行时间</summary>
    /// <param name="crons"></param>
    /// <param name="time"></param>
    /// <returns></returns>
    public static DateTime GetPrevious(String[] crons, DateTime time)
    {
        var prev = DateTime.MinValue;
        foreach (var item in crons)
        {
            var cron = new Cron(item);
            var dt = cron.GetPrevious(time);
            if (dt > prev) prev = dt;
        }
        return prev;
    }
    #endregion
}