diff --git a/AntJob.Agent/AntJob.Agent.csproj b/AntJob.Agent/AntJob.Agent.csproj index 9ec6290..69172a0 100644 --- a/AntJob.Agent/AntJob.Agent.csproj +++ b/AntJob.Agent/AntJob.Agent.csproj @@ -32,7 +32,7 @@ - + diff --git a/AntJob.Data/AntJob.Data.csproj b/AntJob.Data/AntJob.Data.csproj index 30f45ed..b7ba766 100644 --- a/AntJob.Data/AntJob.Data.csproj +++ b/AntJob.Data/AntJob.Data.csproj @@ -37,7 +37,7 @@ - + diff --git a/AntJob.Extensions/AntJob.Extensions.csproj b/AntJob.Extensions/AntJob.Extensions.csproj index d6792d9..c1b6e3c 100644 --- a/AntJob.Extensions/AntJob.Extensions.csproj +++ b/AntJob.Extensions/AntJob.Extensions.csproj @@ -43,7 +43,7 @@ - + diff --git a/AntJob.Web/AntJob.Web.csproj b/AntJob.Web/AntJob.Web.csproj index c31ac5c..d122f70 100644 --- a/AntJob.Web/AntJob.Web.csproj +++ b/AntJob.Web/AntJob.Web.csproj @@ -48,7 +48,7 @@ - + diff --git a/AntJob/AntJob.csproj b/AntJob/AntJob.csproj index 31f367b..60a15bb 100644 --- a/AntJob/AntJob.csproj +++ b/AntJob/AntJob.csproj @@ -49,9 +49,9 @@ - - - + + + diff --git a/AntJob/Data/TemplateHelper.cs b/AntJob/Data/TemplateHelper.cs index 4b7bb39..c373e9f 100644 --- a/AntJob/Data/TemplateHelper.cs +++ b/AntJob/Data/TemplateHelper.cs @@ -21,20 +21,20 @@ public static String Build(String template, DateTime startTime, DateTime endTime while (true) { var ti = Find(str, "DataTime", p); - ti ??= Find(str, "dt", p); - if (ti == null) + if (ti.IsEmpty) ti = Find(str, "dt", p); + if (ti.IsEmpty) { sb.Append(str.Substring(p)); break; } // 准备替换 - var val = ti.Item3.IsNullOrEmpty() ? startTime.ToFullString() : startTime.ToString(ti.Item3); - sb.Append(str.Substring(p, ti.Item1 - p)); + var val = ti.Format.IsNullOrEmpty() ? startTime.ToFullString() : startTime.ToString(ti.Format); + sb.Append(str.Substring(p, ti.Start - p)); sb.Append(val); // 移动指针 - p = ti.Item2 + 1; + p = ti.End + 1; } str = sb.ToString(); @@ -43,32 +43,32 @@ public static String Build(String template, DateTime startTime, DateTime endTime while (true) { var ti = Find(str, "End", p); - if (ti == null) + if (ti.IsEmpty) { sb.Append(str.Substring(p)); break; } // 准备替换 - var val = ti.Item3.IsNullOrEmpty() ? endTime.ToFullString() : endTime.ToString(ti.Item3); - sb.Append(str.Substring(p, ti.Item1 - p)); + var val = ti.Format.IsNullOrEmpty() ? endTime.ToFullString() : endTime.ToString(ti.Format); + sb.Append(str.Substring(p, ti.Start - p)); sb.Append(val); // 移动指针 - p = ti.Item2 + 1; + p = ti.End + 1; } return sb.Put(true); } - private static Tuple Find(String str, String key, Int32 p) + private static VarItem Find(String str, String key, Int32 p) { // 头尾 var p1 = str.IndexOf("{" + key, p); - if (p1 < 0) return null; + if (p1 < 0) return _empty; var p2 = str.IndexOf("}", p1); - if (p2 < 0) return null; + if (p2 < 0) return _empty; // 格式化字符串 var format = ""; @@ -76,7 +76,17 @@ private static Tuple Find(String str, String key, Int32 p) if (p3 > 0 && p3 < p2) format = str.Substring(p3 + 1, p2 - p3 - 1); // 左括号位置,右括号位置,格式化字符串 - return new Tuple(p1, p2, format); + return new VarItem(p1, p2, format); + } + + private static VarItem _empty = new(-1, -1, ""); + struct VarItem(Int32 start, Int32 end, String format) + { + public Int32 Start = start; + public Int32 End = end; + public String Format = format; + + public readonly Boolean IsEmpty => Start < 0; } /// 使用消息数组处理模板 diff --git a/AntJob/Data/TimeExpression.cs b/AntJob/Data/TimeExpression.cs new file mode 100644 index 0000000..a137de8 --- /dev/null +++ b/AntJob/Data/TimeExpression.cs @@ -0,0 +1,170 @@ +using NewLife; + +namespace AntJob.Data; + +/// 时间表达式,一次解析多次使用。如{dt+1M+5d:yyyyMMdd} +public class TimeExpression +{ + #region 属性 + /// 表达式 + public String Expression { get; set; } + + /// 变量名 + public String VarName { get; set; } = "dt"; + + /// 格式化字符串 + public String Format { get; set; } + + /// 时间表达式项集合 + public IList Items { get; } = []; + #endregion + + #region 构造 + /// 实例化时间表达式 + public TimeExpression() { } + + /// 实例化时间表达式 + public TimeExpression(String expression) => Parse(expression); + #endregion + + #region 方法 + /// 解析表达式 + public Boolean Parse(String expression) + { + var p1 = expression.IndexOf('{'); + if (p1 < 0) return false; + + var p2 = expression.IndexOf('}', p1); + if (p2 < 0) return false; + + expression = expression.Substring(p1 + 1, p2 - p1 - 1); + + // 循环查找 + var ms = Items; + p1 = -1; + while (true) + { + p2 = expression.IndexOfAny(['+', '-', ':', ','], p1 + 1); + if (p2 < 0) p2 = expression.Length; + + // 第一段是变量名 + if (p1 < 0 && p2 > 0) + { + VarName = expression[0..p2]; + } + else if (expression[p1] is '+' or '-') + { + var str = expression[p1..p2]; + var item = new TimeExpressionItem(); + if (!item.Parse(str)) throw new InvalidDataException($"Invalid [{str}]"); + + ms.Add(item); + } + else if (expression[p1] is ':' or ',') + { + // 最后一段是格式化字符串 + p2 = expression.Length; + Format = expression[(p1 + 1)..p2]; + } + + if (p2 >= expression.Length) break; + p1 = p2; + } + + // 默认天级 + if (ms.Count == 0) ms.Add(new TimeExpressionItem { Level = "d", Value = 0 }); + + Expression = expression; + + return true; + } + + /// 执行时间偏移 + public DateTime Execute(DateTime time) + { + foreach (var item in Items) + { + time = item.Execute(time); + } + + return time; + } + + /// 构建时间字符串 + public String Build(DateTime time) + { + time = Execute(time); + + var ms = Items; + var format = Format; + if (format.IsNullOrEmpty() && ms.Count > 0) format = ms[ms.Count - 1].GetFormat(); + if (format.IsNullOrEmpty()) format = "yyyyMMdd"; + + return time.ToString(format); + } + #endregion + + /// 处理时间偏移模版。如{dt+1M+5d:yyyyMMdd} + /// + /// + /// + public static String Build(String template, DateTime time) + { + return null; + } +} + +/// 时间表达式项。如+5d +public class TimeExpressionItem +{ + /// 级别。如y/M/d/H/m/w + public String Level { get; set; } + + /// 数值。包括正负 + public Int32 Value { get; set; } + + /// 分解表达式项。如+5d + /// + /// + public Boolean Parse(String value) + { + if (value.IsNullOrEmpty() || value.Length < 3) return false; + + Level = value[^1..]; + Value = value[..^1].ToInt(); + + return true; + } + + /// 执行时间偏移 + public DateTime Execute(DateTime time) + { + return Level switch + { + "y" => new DateTime(time.Year, 1, 1, 0, 0, 0, time.Kind).AddYears(Value), + "M" => new DateTime(time.Year, time.Month, 1, 0, 0, 0, time.Kind).AddMonths(Value), + "d" => new DateTime(time.Year, time.Month, time.Day, 0, 0, 0, time.Kind).AddDays(Value), + "H" => new DateTime(time.Year, time.Month, time.Day, time.Hour, 0, 0, time.Kind).AddHours(Value), + "m" => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, 0, time.Kind).AddMinutes(Value), + "s" => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Kind).AddSeconds(Value), + "w" => new DateTime(time.Year, time.Month, time.Day, 0, 0, 0, time.Kind).AddDays(Value * 7), + _ => time, + }; + } + + /// 获取格式化字符串 + public String GetFormat() + { + return Level switch + { + "y" => "yyyy", + "M" => "yyyyMM", + "d" => "yyyyMMdd", + "H" => "yyyyMMddHH", + "m" => "yyyyMMddHHmm", + "s" => "yyyyMMddHHmmss", + "w" => "yyyyww", + _ => "", + }; + } +} \ No newline at end of file diff --git a/AntTest/TimeExpressionTests.cs b/AntTest/TimeExpressionTests.cs new file mode 100644 index 0000000..c233a28 --- /dev/null +++ b/AntTest/TimeExpressionTests.cs @@ -0,0 +1,110 @@ +using System; +using AntJob.Data; +using Xunit; + +namespace AntTest; + +public class TimeExpressionTests +{ + [Theory] + [InlineData("+1y", "y", 1)] + [InlineData("-1y", "y", -1)] + [InlineData("+5M", "M", 5)] + [InlineData("-5M", "M", -5)] + [InlineData("+5d", "d", 5)] + [InlineData("-5d", "d", -5)] + [InlineData("-0d", "d", 0)] + [InlineData("+5H", "H", 5)] + [InlineData("-5H", "H", -5)] + [InlineData("+5m", "m", 5)] + [InlineData("-5m", "m", -5)] + [InlineData("+5w", "w", 5)] + [InlineData("-5w", "w", -5)] + public void ParseItem(String str, String level, Int32 value) + { + var item = new TimeExpressionItem(); + var rs = item.Parse(str); + Assert.True(rs); + Assert.Equal(level, item.Level); + Assert.Equal(value, item.Value); + + var time = DateTime.Now; + var time2 = item.Execute(time); + + if (value > 0) + Assert.True(time2 > time); + else if (value < 0) + Assert.True(time2 < time); + } + + [Fact] + public void TestDefault() + { + var exp = new TimeExpression("${dt}"); + Assert.Equal("dt", exp.Expression); + Assert.Equal("dt", exp.VarName); + Assert.Null(exp.Format); + Assert.Single(exp.Items); + + var time = DateTime.Now; + var time2 = exp.Execute(time); + Assert.Equal(time.Date, time2); + + var rs = exp.Build(time); + Assert.Equal(time.ToString("yyyyMMdd"), rs); + } + + [Fact] + public void Test2() + { + var exp = new TimeExpression("${dt+2d}"); + Assert.Equal("dt+2d", exp.Expression); + Assert.Equal("dt", exp.VarName); + Assert.Null(exp.Format); + Assert.Single(exp.Items); + + var time = DateTime.Now; + var time2 = exp.Execute(time); + var time3 = time.Date.AddDays(2); + Assert.Equal(time3, time2); + + var rs = exp.Build(time); + Assert.Equal(time3.ToString("yyyyMMdd"), rs); + } + + [Fact] + public void Test3() + { + var exp = new TimeExpression("${dt-3H:yyMMddHH}"); + Assert.Equal("dt-3H:yyMMddHH", exp.Expression); + Assert.Equal("dt", exp.VarName); + Assert.Equal("yyMMddHH", exp.Format); + Assert.Single(exp.Items); + + var time = DateTime.Now; + var time2 = exp.Execute(time); + var time3 = time.Date.AddHours(time.Hour - 3); + Assert.Equal(time3, time2); + + var rs = exp.Build(time); + Assert.Equal(time3.ToString("yyMMddHH"), rs); + } + + [Fact] + public void Test4() + { + var exp = new TimeExpression("${dt+1M+4d:yy-MM-dd}"); + Assert.Equal("dt+1M+4d:yy-MM-dd", exp.Expression); + Assert.Equal("dt", exp.VarName); + Assert.Equal("yy-MM-dd", exp.Format); + Assert.Equal(2, exp.Items.Count); + + var time = DateTime.Now; + var time2 = exp.Execute(time); + var time3 = time.Date.AddDays(1 - time.Day).AddMonths(1).AddDays(4); + Assert.Equal(time3, time2); + + var rs = exp.Build(time); + Assert.Equal(time3.ToString("yy-MM-dd"), rs); + } +} diff --git a/Samples/HisData/HisData.csproj b/Samples/HisData/HisData.csproj index ac58bfc..a5d272f 100644 --- a/Samples/HisData/HisData.csproj +++ b/Samples/HisData/HisData.csproj @@ -16,7 +16,7 @@ - +