/* SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. * * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ using System; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Text.RegularExpressions; using OpenSearch.Net.Extensions; using OpenSearch.Net.Utf8Json; namespace OpenSearch.Client { public interface IDateMath { Union<DateTime, string> Anchor { get; } IList<Tuple<DateMathOperation, DateMathTime>> Ranges { get; } DateMathTimeUnit? Round { get; } } [JsonFormatter(typeof(DateMathFormatter))] public abstract class DateMath : IDateMath { private static readonly Regex DateMathRegex = new Regex(@"^(?<anchor>now|.+(?:\|\||$))(?<ranges>(?:(?:\+|\-)[^\/]*))?(?<rounding>\/(?:y|M|w|d|h|m|s))?$"); protected Union<DateTime, string> Anchor; protected DateMathTimeUnit? Round; public static DateMathExpression Now => new DateMathExpression("now"); protected IDateMath Self => this; Union<DateTime, string> IDateMath.Anchor => Anchor; IList<Tuple<DateMathOperation, DateMathTime>> IDateMath.Ranges { get; } = new List<Tuple<DateMathOperation, DateMathTime>>(); DateMathTimeUnit? IDateMath.Round => Round; public static DateMathExpression Anchored(DateTime anchor) => new DateMathExpression(anchor); public static DateMathExpression Anchored(string dateAnchor) => new DateMathExpression(dateAnchor); public static implicit operator DateMath(DateTime dateTime) => Anchored(dateTime); public static implicit operator DateMath(string dateMath) => FromString(dateMath); public static DateMath FromString(string dateMath) { if (dateMath == null) return null; var match = DateMathRegex.Match(dateMath); if (!match.Success) throw new ArgumentException($"Cannot create a {nameof(DateMathExpression)} out of '{dateMath}'"); var math = new DateMathExpression(match.Groups["anchor"].Value); if (match.Groups["ranges"].Success) { var rangeString = match.Groups["ranges"].Value; do { var nextRangeStart = rangeString.Substring(1).IndexOfAny(new[] { '+', '-', '/' }); if (nextRangeStart == -1) nextRangeStart = rangeString.Length - 1; var unit = rangeString.Substring(1, nextRangeStart); if (rangeString.StartsWith("+", StringComparison.Ordinal)) { math = math.Add(unit); rangeString = rangeString.Substring(nextRangeStart + 1); } else if (rangeString.StartsWith("-", StringComparison.Ordinal)) { math = math.Subtract(unit); rangeString = rangeString.Substring(nextRangeStart + 1); } else rangeString = null; } while (!rangeString.IsNullOrEmpty()); } if (match.Groups["rounding"].Success) { var rounding = match.Groups["rounding"].Value.Substring(1).ToEnum<DateMathTimeUnit>(StringComparison.Ordinal); if (rounding.HasValue) return math.RoundTo(rounding.Value); } return math; } internal static bool IsValidDateMathString(string dateMath) => dateMath != null && DateMathRegex.IsMatch(dateMath); internal bool IsValid => Self.Anchor.Match(_ => true, s => !s.IsNullOrEmpty()); public override string ToString() { if (!IsValid) return string.Empty; var separator = Self.Round.HasValue || Self.Ranges.HasAny() ? "||" : string.Empty; var sb = new StringBuilder(); var anchor = Self.Anchor.Match( d => ToMinThreeDecimalPlaces(d) + separator, s => s == "now" || s.EndsWith("||", StringComparison.Ordinal) ? s : s + separator ); sb.Append(anchor); foreach (var r in Self.Ranges) { sb.Append(r.Item1.GetStringValue()); //date math does not support fractional time units so e.g TimeSpan.FromHours(25) should not yield 1.04d sb.Append(r.Item2); } if (Self.Round.HasValue) sb.Append("/" + Self.Round.Value.GetStringValue()); return sb.ToString(); } /// <summary> /// Formats a <see cref="DateTime"/> to have a minimum of 3 decimal places if there are sub second values /// </summary> private static string ToMinThreeDecimalPlaces(DateTime dateTime) { var builder = StringBuilderCache.Acquire(33); var format = dateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFF", CultureInfo.InvariantCulture); builder.Append(format); // Fixes bug in Elasticsearch: https://github.com/elastic/elasticsearch/pull/41871 if (format.Length > 20 && format.Length < 23) { var diff = 23 - format.Length; for (var i = 0; i < diff; i++) builder.Append('0'); } switch (dateTime.Kind) { case DateTimeKind.Local: var offset = TimeZoneInfo.Local.GetUtcOffset(dateTime); if (offset >= TimeSpan.Zero) builder.Append('+'); else { builder.Append('-'); offset = offset.Negate(); } AppendTwoDigitNumber(builder, offset.Hours); builder.Append(':'); AppendTwoDigitNumber(builder, offset.Minutes); break; case DateTimeKind.Utc: builder.Append('Z'); break; } return StringBuilderCache.GetStringAndRelease(builder); } private static void AppendTwoDigitNumber(StringBuilder result, int val) { result.Append((char)('0' + (val / 10))); result.Append((char)('0' + (val % 10))); } } internal class DateMathFormatter : IJsonFormatter<DateMath> { public DateMath Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) { var token = reader.GetCurrentJsonToken(); if (token != JsonToken.String) return null; var segment = reader.ReadStringSegmentUnsafe(); if (!segment.ContainsDateMathSeparator() && segment.IsDateTime(formatterResolver, out var dateTime)) return DateMath.Anchored(dateTime); var value = segment.Utf8String(); return DateMath.FromString(value); } public void Serialize(ref JsonWriter writer, DateMath value, IJsonFormatterResolver formatterResolver) => writer.WriteString(value.ToString()); } }