Skip to content

Commit 926d6fb

Browse files
stevejgordongithub-actions[bot]
authored andcommitted
Port DateMath and update codegen (#6521)
* Port DateMath and related types * Updated code-gen to use DateMath alias * Add tests and fix serialization * Fix license headers
1 parent 7993d73 commit 926d6fb

File tree

14 files changed

+1100
-28
lines changed

14 files changed

+1100
-28
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Globalization;
8+
using System.Text;
9+
using System.Text.Json;
10+
using System.Text.Json.Serialization;
11+
using System.Text.RegularExpressions;
12+
13+
namespace Elastic.Clients.Elasticsearch;
14+
15+
[JsonConverter(typeof(DateMathConverter))]
16+
public abstract class DateMath
17+
{
18+
private static readonly Regex DateMathRegex =
19+
new(@"^(?<anchor>now|.+(?:\|\||$))(?<ranges>(?:(?:\+|\-)[^\/]*))?(?<rounding>\/(?:y|M|w|d|h|m|s))?$");
20+
21+
public static DateMathExpression Now => new("now");
22+
23+
internal DateMath(string anchor) => Anchor = anchor;
24+
internal DateMath(DateTime anchor) => Anchor = anchor;
25+
internal DateMath(Union<DateTime, string> anchor, DateMathTime range, DateMathOperation operation)
26+
{
27+
anchor.ThrowIfNull(nameof(anchor));
28+
range.ThrowIfNull(nameof(range));
29+
operation.ThrowIfNull(nameof(operation));
30+
Anchor = anchor;
31+
Ranges.Add(Tuple.Create(operation, range));
32+
}
33+
34+
public Union<DateTime, string> Anchor { get; }
35+
public IList<Tuple<DateMathOperation, DateMathTime>> Ranges { get; } = new List<Tuple<DateMathOperation, DateMathTime>>();
36+
public DateMathTimeUnit? Round { get; protected set; }
37+
38+
public static DateMathExpression Anchored(DateTime anchor) => new(anchor);
39+
40+
public static DateMathExpression Anchored(string dateAnchor) => new(dateAnchor);
41+
42+
public static implicit operator DateMath(DateTime dateTime) => Anchored(dateTime);
43+
44+
public static implicit operator DateMath(string dateMath) => FromString(dateMath);
45+
46+
public static DateMath FromString(string dateMath)
47+
{
48+
if (dateMath == null) return null;
49+
50+
var match = DateMathRegex.Match(dateMath);
51+
if (!match.Success) throw new ArgumentException($"Cannot create a {nameof(DateMathExpression)} out of '{dateMath}'");
52+
53+
var math = new DateMathExpression(match.Groups["anchor"].Value);
54+
55+
if (match.Groups["ranges"].Success)
56+
{
57+
var rangeString = match.Groups["ranges"].Value;
58+
do
59+
{
60+
var nextRangeStart = rangeString.Substring(1).IndexOfAny(new[] { '+', '-', '/' });
61+
if (nextRangeStart == -1) nextRangeStart = rangeString.Length - 1;
62+
var unit = rangeString.Substring(1, nextRangeStart);
63+
if (rangeString.StartsWith("+", StringComparison.Ordinal))
64+
{
65+
math = math.Add(unit);
66+
rangeString = rangeString.Substring(nextRangeStart + 1);
67+
}
68+
else if (rangeString.StartsWith("-", StringComparison.Ordinal))
69+
{
70+
math = math.Subtract(unit);
71+
rangeString = rangeString.Substring(nextRangeStart + 1);
72+
}
73+
else rangeString = null;
74+
} while (!rangeString.IsNullOrEmpty());
75+
}
76+
77+
if (match.Groups["rounding"].Success)
78+
{
79+
var rounding = match.Groups["rounding"].Value.Substring(1).ToEnum<DateMathTimeUnit>(StringComparison.Ordinal);
80+
if (rounding.HasValue)
81+
return math.RoundTo(rounding.Value);
82+
}
83+
return math;
84+
}
85+
86+
internal static bool IsValidDateMathString(string dateMath) => dateMath != null && DateMathRegex.IsMatch(dateMath);
87+
88+
internal bool IsValid => Anchor.Match(_ => true, s => !s.IsNullOrEmpty());
89+
90+
public override string ToString()
91+
{
92+
if (!IsValid) return string.Empty;
93+
94+
var separator = Round.HasValue || Ranges.HasAny() ? "||" : string.Empty;
95+
96+
var sb = new StringBuilder();
97+
var anchor = Anchor.Match(
98+
d => ToMinThreeDecimalPlaces(d) + separator,
99+
s => s == "now" || s.EndsWith("||", StringComparison.Ordinal) ? s : s + separator
100+
);
101+
sb.Append(anchor);
102+
foreach (var r in Ranges)
103+
{
104+
sb.Append(r.Item1.GetStringValue());
105+
//date math does not support fractional time units so e.g TimeSpan.FromHours(25) should not yield 1.04d
106+
sb.Append(r.Item2);
107+
}
108+
if (Round.HasValue)
109+
sb.Append("/" + Round.Value.GetStringValue());
110+
111+
return sb.ToString();
112+
}
113+
114+
/// <summary>
115+
/// Formats a <see cref="DateTime"/> to have a minimum of 3 decimal places if there are sub second values
116+
/// </summary>
117+
private static string ToMinThreeDecimalPlaces(DateTime dateTime)
118+
{
119+
var builder = new StringBuilder(33);
120+
var format = dateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFF", CultureInfo.InvariantCulture);
121+
builder.Append(format);
122+
123+
if (format.Length > 20 && format.Length < 23)
124+
{
125+
var diff = 23 - format.Length;
126+
for (var i = 0; i < diff; i++)
127+
builder.Append('0');
128+
}
129+
130+
switch (dateTime.Kind)
131+
{
132+
case DateTimeKind.Local:
133+
var offset = TimeZoneInfo.Local.GetUtcOffset(dateTime);
134+
if (offset >= TimeSpan.Zero)
135+
builder.Append('+');
136+
else
137+
{
138+
builder.Append('-');
139+
offset = offset.Negate();
140+
}
141+
142+
AppendTwoDigitNumber(builder, offset.Hours);
143+
builder.Append(':');
144+
AppendTwoDigitNumber(builder, offset.Minutes);
145+
break;
146+
case DateTimeKind.Utc:
147+
builder.Append('Z');
148+
break;
149+
}
150+
151+
return builder.ToString();
152+
}
153+
154+
private static void AppendTwoDigitNumber(StringBuilder result, int val)
155+
{
156+
result.Append((char)('0' + (val / 10)));
157+
result.Append((char)('0' + (val % 10)));
158+
}
159+
}
160+
161+
internal sealed class DateMathConverter : JsonConverter<DateMath>
162+
{
163+
public override DateMath? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
164+
{
165+
if (reader.TokenType != JsonTokenType.String)
166+
return null;
167+
168+
// TODO: Performance - Review potential to avoid allocation on DateTime path and use Span<byte>
169+
170+
var value = reader.GetString();
171+
reader.Read();
172+
173+
if (!value.Contains("|") && DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTime))
174+
return DateMath.Anchored(dateTime);
175+
176+
return DateMath.Anchored(value);
177+
}
178+
179+
public override void Write(Utf8JsonWriter writer, DateMath value, JsonSerializerOptions options)
180+
{
181+
if (value is null)
182+
{
183+
writer.WriteNullValue();
184+
return;
185+
}
186+
187+
writer.WriteStringValue(value.ToString());
188+
}
189+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Globalization;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
10+
namespace Elastic.Clients.Elasticsearch;
11+
12+
[JsonConverter(typeof(DateMathExpressionConverter))]
13+
public class DateMathExpression : DateMath
14+
{
15+
public DateMathExpression(string anchor) : base(anchor) { }
16+
17+
public DateMathExpression(DateTime anchor) : base(anchor) { }
18+
19+
public DateMathExpression(Union<DateTime, string> anchor, DateMathTime range, DateMathOperation operation)
20+
: base(anchor, range, operation) { }
21+
22+
public DateMathExpression Add(DateMathTime expression)
23+
{
24+
Ranges.Add(Tuple.Create(DateMathOperation.Add, expression));
25+
return this;
26+
}
27+
28+
public DateMathExpression Subtract(DateMathTime expression)
29+
{
30+
Ranges.Add(Tuple.Create(DateMathOperation.Subtract, expression));
31+
return this;
32+
}
33+
34+
public DateMathExpression Operation(DateMathTime expression, DateMathOperation operation)
35+
{
36+
Ranges.Add(Tuple.Create(operation, expression));
37+
return this;
38+
}
39+
40+
public DateMath RoundTo(DateMathTimeUnit round)
41+
{
42+
Round = round;
43+
return this;
44+
}
45+
}
46+
47+
internal sealed class DateMathExpressionConverter : JsonConverter<DateMathExpression>
48+
{
49+
public override DateMathExpression? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
50+
{
51+
if (reader.TokenType != JsonTokenType.String)
52+
return null;
53+
54+
// TODO: Performance - Review potential to avoid allocation on DateTime path and use Span<byte>
55+
56+
var value = reader.GetString();
57+
reader.Read();
58+
59+
if (!value.Contains("|") && DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTime))
60+
return DateMath.Anchored(dateTime);
61+
62+
return new DateMathExpression(value);
63+
}
64+
65+
public override void Write(Utf8JsonWriter writer, DateMathExpression value, JsonSerializerOptions options)
66+
{
67+
if (value is null)
68+
{
69+
writer.WriteNullValue();
70+
return;
71+
}
72+
73+
writer.WriteStringValue(value.ToString());
74+
}
75+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Runtime.Serialization;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
10+
namespace Elastic.Clients.Elasticsearch;
11+
12+
[StringEnum]
13+
[JsonConverter(typeof(DateMathOperationConverter))]
14+
public enum DateMathOperation
15+
{
16+
[EnumMember(Value = "+")]
17+
Add,
18+
19+
[EnumMember(Value = "-")]
20+
Subtract
21+
}
22+
23+
internal sealed class DateMathOperationConverter : JsonConverter<DateMathOperation>
24+
{
25+
public override DateMathOperation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
26+
{
27+
var enumString = reader.GetString();
28+
switch (enumString)
29+
{
30+
case "+":
31+
return DateMathOperation.Add;
32+
case "-":
33+
return DateMathOperation.Subtract;
34+
}
35+
36+
ThrowHelper.ThrowJsonException();
37+
return default;
38+
}
39+
40+
public override void Write(Utf8JsonWriter writer, DateMathOperation value, JsonSerializerOptions options)
41+
{
42+
switch (value)
43+
{
44+
case DateMathOperation.Add:
45+
writer.WriteStringValue("+");
46+
return;
47+
case DateMathOperation.Subtract:
48+
writer.WriteStringValue("-");
49+
return;
50+
}
51+
52+
writer.WriteNullValue();
53+
}
54+
}
55+
56+
public static class DateMathOperationExtensions
57+
{
58+
public static string GetStringValue(this DateMathOperation value)
59+
{
60+
switch (value)
61+
{
62+
case DateMathOperation.Add:
63+
return "+";
64+
case DateMathOperation.Subtract:
65+
return "-";
66+
default:
67+
throw new ArgumentOutOfRangeException(nameof(value), value, null);
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)