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 @@
-
+