对时间游戏充满了兴趣。时间这个话题在职业生涯中也伴随着很长时间,处理过复杂的时间计算。最后一个开源项目是关于能够在字段级别控制业务数据的生命周期。由于额外的业务需求,已建立的历史化和审计跟踪方法不再适用:数据是在字段级别而不是对象级别控制的;数据可以多次更改;数据可以追溯性地更改;数据更改可以安排;使用过去和(回到)未来的数据分析。
以员工工资为例,将展示是如何用时间数据解决这个问题的。在初始情况下,有员工name@domain.com,月薪5000。
{
"id": "name@domain.com",
"salary": 5000
}
时间数据除了实际值外,还包含创建日期和值适用的时间范围。上述示例的时间数据和额外的时间限制直到2023年底:
{
"id": "name@domain.com",
"salary": [
{
"value": 5000,
"created": "2023-01-01T00:00:00.0Z",
"period": {
"start": "2023-01-01T00:00:00.0Z",
"end": "2023-12-31T23:59:59.9Z"
}
}
]
}
时间数据是不可变的,所以下次调整工资时会创建一个新的时间值。在以下示例中,从3月到8月底,工资提高到6000。
{
"id": "name@domain.com",
"salary": [
{
"value": 5000,
"created": "2023-01-01T00:00:00.0Z",
"period": {
"start": "2023-01-01T00:00:00.0Z"
}
},
{
"value": 6000,
"created": "2023-03-01T00:00:00.0Z",
"period": {
"start": "2023-03-01T00:00:00.0Z",
"end": "2023-08-31T23:59:59.9Z"
}
}
]
}
有了这些信息,可以在任何时间点确定值。时间数据确实开辟了新的视角:可以自由选择一个时间点,并从那个时间点开始查看数据。将时间值的观察日期命名为评估日期。
有了可变的评估日期,可以进行有趣的评估:过去视图 - 分析早期的数据;未来视图 - 分析未来的数据。下图显示了上述示例的不同评估日期:
在这个例子中,2023年3月1日工资发生了变化,追溯到2月。新的工资5500将一直有效到2023年4月中旬。
{
"id": "name@domain.com",
"salary": [
{
"value": 5000,
"created": "2023-01-01T00:00:00.0Z",
"period": {
"start": "2023-01-01T00:00:00.0Z"
}
},
{
"value": 5500,
"created": "2023-03-01T00:00:00.0Z",
"period": {
"start": "2023-02-01T00:00:00.0Z",
"end": "2023-04-15T23:59:59.9Z"
}
}
]
}
评估时间数据值如下:
在这个场景中,2023年3月1日工资提高到4500。调整从9月到10月中旬有效。
{
"id": "name@domain.com",
"salary": [
{
"value": 5000,
"created": "2023-01-01T00:00:00.0Z",
"period": {
"start": "2023-01-01T00:00:00.0Z"
}
},
{
"value": 5500,
"created": "2023-03-01T00:00:00.0Z",
"period": {
"start": "2023-08-01T00:00:00.0Z",
"end": "2023-10-15T23:59:59.9Z"
}
}
]
}
这导致以下时间数据值:
时间数据与C#
示例包含以下类型:
TimeValue - 包含时间数据
TimeField - 带有多个时间数据的时间字段
Employee - 带有工资作为时间数据的员工
Extensions - 时间数据计算
public class TimeValue
{
public DateTime Created { get; set; }
public ValuePeriod Period { get; set; }
public T Value { get; set; }
}
public class TimeField : Collection>
{
}
public class Employee
{
public string? Id { get; set; }
public TimeField? Salary { get; set; }
}
public static class Extensions
{
public static bool IsInside(this TimeValue value, DateTime evaluationDate) =>
evaluationDate >= value.Period.Start && evaluationDate <= value.Period.End;
public static TimeValue? GetTimeValue(this TimeField values, DateTime evaluationDate)
{
if (!values.Any())
{
return default;
}
var timeValues = values.
// remove values created after the evaluation date
Where(x => x.Created <= evaluationDate &&
// remove outside periods
x.IsInside(evaluationDate)).ToList();
if (!timeValues.Any())
{
return default;
}
// select the evaluated value (last created)
var timeValue = timeValues.OrderByDescending(x => x.Created).First();
return timeValue;
}
}
计算当前时间值的算法GetTimeValue首先排除在评估时间之后创建的时间值或不相关的时间值(IsInside)。从剩余的时间值中,选择最近的一个。
在关系模型中,时间数据可以保存在单独的实体/表中。如果时间数据量小,最简单的场景是有一个单独的表包含时间数据,例如EmployeeSalary。对于大量时间数据,建议将它们分组到表中。