[feat]新增时间表达式TimeExpression,一次解析多次使用,支持${dt+1M+4d:yy-MM-dd}

This commit is contained in:
大石头 2024-08-06 11:43:51 +08:00
parent 6225663bff
commit 7bd9536756
9 changed files with 311 additions and 21 deletions

View File

@ -32,7 +32,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NewLife.Stardust" Version="3.0.2024.802" />
<PackageReference Include="NewLife.Stardust" Version="3.0.2024.806" />
</ItemGroup>
<ItemGroup>

View File

@ -37,7 +37,7 @@
<None Remove="Build.tt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NewLife.XCode" Version="11.15.2024.801" />
<PackageReference Include="NewLife.XCode" Version="11.15.2024.806" />
</ItemGroup>
<ItemGroup>
<None Update="Build.log">

View File

@ -43,7 +43,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NewLife.XCode" Version="11.15.2024.801" />
<PackageReference Include="NewLife.XCode" Version="11.15.2024.806" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AntJob\AntJob.csproj" />

View File

@ -48,7 +48,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NewLife.Cube.Core" Version="6.1.2024.728-beta1411" />
<PackageReference Include="NewLife.Stardust.Extensions" Version="3.0.2024.802" />
<PackageReference Include="NewLife.Stardust.Extensions" Version="3.0.2024.806" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AntJob.Data\AntJob.Data.csproj" />

View File

@ -49,9 +49,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NewLife.Core" Version="10.10.2024.801" />
<PackageReference Include="NewLife.Remoting" Version="3.0.2024.801" />
<PackageReference Include="NewLife.Stardust" Version="3.0.2024.802" />
<PackageReference Include="NewLife.Core" Version="10.10.2024.803" />
<PackageReference Include="NewLife.Remoting" Version="3.0.2024.805" />
<PackageReference Include="NewLife.Stardust" Version="3.0.2024.806" />
</ItemGroup>
<ItemGroup>

View File

@ -21,20 +21,20 @@ public static class TemplateHelper
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 class TemplateHelper
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<Int32, Int32, String> 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 @@ public static class TemplateHelper
if (p3 > 0 && p3 < p2) format = str.Substring(p3 + 1, p2 - p3 - 1);
// 左括号位置,右括号位置,格式化字符串
return new Tuple<Int32, Int32, String>(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;
}
/// <summary>使用消息数组处理模板</summary>

View File

@ -0,0 +1,170 @@
using NewLife;
namespace AntJob.Data;
/// <summary>时间表达式,一次解析多次使用。如{dt+1M+5d:yyyyMMdd}</summary>
public class TimeExpression
{
#region
/// <summary>表达式</summary>
public String Expression { get; set; }
/// <summary>变量名</summary>
public String VarName { get; set; } = "dt";
/// <summary>格式化字符串</summary>
public String Format { get; set; }
/// <summary>时间表达式项集合</summary>
public IList<TimeExpressionItem> Items { get; } = [];
#endregion
#region
/// <summary>实例化时间表达式</summary>
public TimeExpression() { }
/// <summary>实例化时间表达式</summary>
public TimeExpression(String expression) => Parse(expression);
#endregion
#region
/// <summary>解析表达式</summary>
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;
}
/// <summary>执行时间偏移</summary>
public DateTime Execute(DateTime time)
{
foreach (var item in Items)
{
time = item.Execute(time);
}
return time;
}
/// <summary>构建时间字符串</summary>
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
/// <summary>处理时间偏移模版。如{dt+1M+5d:yyyyMMdd}</summary>
/// <param name="template"></param>
/// <param name="time"></param>
/// <returns></returns>
public static String Build(String template, DateTime time)
{
return null;
}
}
/// <summary>时间表达式项。如+5d</summary>
public class TimeExpressionItem
{
/// <summary>级别。如y/M/d/H/m/w</summary>
public String Level { get; set; }
/// <summary>数值。包括正负</summary>
public Int32 Value { get; set; }
/// <summary>分解表达式项。如+5d</summary>
/// <param name="value"></param>
/// <returns></returns>
public Boolean Parse(String value)
{
if (value.IsNullOrEmpty() || value.Length < 3) return false;
Level = value[^1..];
Value = value[..^1].ToInt();
return true;
}
/// <summary>执行时间偏移</summary>
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,
};
}
/// <summary>获取格式化字符串</summary>
public String GetFormat()
{
return Level switch
{
"y" => "yyyy",
"M" => "yyyyMM",
"d" => "yyyyMMdd",
"H" => "yyyyMMddHH",
"m" => "yyyyMMddHHmm",
"s" => "yyyyMMddHHmmss",
"w" => "yyyyww",
_ => "",
};
}
}

View File

@ -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);
}
}

View File

@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NewLife.XCode" Version="11.15.2024.801" />
<PackageReference Include="NewLife.XCode" Version="11.15.2024.806" />
</ItemGroup>
<ItemGroup>