/* 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.Globalization; using System.Text.RegularExpressions; using OpenSearch.Net.Utf8Json; namespace OpenSearch.Client { /// /// A time representation for use within expressions. /// [JsonFormatter(typeof(DateMathTimeFormatter))] public class DateMathTime : IComparable, IEquatable { private const double MillisecondsInADay = MillisecondsInAnHour * 24; private const double MillisecondsInAMinute = MillisecondsInASecond * 60; private const double MillisecondsInAMonthApproximate = MillisecondsInAYearApproximate / MonthsInAYear; private const double MillisecondsInAnHour = MillisecondsInAMinute * 60; private const double MillisecondsInASecond = 1000; private const double MillisecondsInAWeek = MillisecondsInADay * 7; private const double MillisecondsInAYearApproximate = MillisecondsInADay * 365; private const int MonthsInAYear = 12; private static readonly Regex ExpressionRegex = new Regex(@"^ (?[+\-]? # open factor capture, allowing optional +- signs (?:(?#numeric)(?:\d+(?:\.\d*)?)|(?:\.\d+)) #a numeric in the forms: (N, N., .N, N.N) (?:(?#exponent)e[+\-]?\d+)? #an optional exponential scientific component, E also matches here (IgnoreCase) ) # numeric and exponent fall under the factor capture \s{0,10} #optional spaces (sanity checked for max 10 repetitions) (?(?:y|w|d|h|m|s)) #interval indicator $", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); private double _approximateSeconds; /// /// Instantiates a new instance of from a TimeSpan. /// Rounding can be specified to determine how fractional second values should be rounded. /// public DateMathTime(TimeSpan timeSpan, MidpointRounding rounding = MidpointRounding.AwayFromZero) : this(timeSpan.TotalMilliseconds, rounding) { } /// /// Instantiates a new instance of from a milliseconds value. /// Rounding can be specified to determine how fractional second values should be rounded. /// public DateMathTime(double milliseconds, MidpointRounding rounding = MidpointRounding.AwayFromZero) => SetWholeFactorIntervalAndSeconds(milliseconds, rounding); /// /// Instantiates a new instance of from a factor and interval. /// public DateMathTime(int factor, DateMathTimeUnit interval) => SetWholeFactorIntervalAndSeconds(factor, interval, MidpointRounding.AwayFromZero); /// /// Instantiates a new instance of from the timeUnit string expression. /// Rounding can be specified to determine how fractional second values should be rounded. /// public DateMathTime(string timeUnit, MidpointRounding rounding = MidpointRounding.AwayFromZero) { if (timeUnit == null) throw new ArgumentNullException(nameof(timeUnit)); if (timeUnit.Length == 0) throw new ArgumentException("Expression string is empty", nameof(timeUnit)); var match = ExpressionRegex.Match(timeUnit); if (!match.Success) throw new ArgumentException($"Expression '{timeUnit}' string is invalid", nameof(timeUnit)); var factor = match.Groups["factor"].Value; if (!double.TryParse(factor, NumberStyles.Any, CultureInfo.InvariantCulture, out var fraction)) throw new ArgumentException($"Expression '{timeUnit}' contains invalid factor: {factor}", nameof(timeUnit)); var intervalValue = match.Groups["interval"].Value; DateMathTimeUnit interval; switch (intervalValue) { case "M": interval = DateMathTimeUnit.Month; break; case "m": interval = DateMathTimeUnit.Minute; break; default: interval = intervalValue.ToEnum().GetValueOrDefault(); break; } SetWholeFactorIntervalAndSeconds(fraction, interval, rounding); } /// /// The numeric time factor /// public int Factor { get; private set; } /// /// The time units /// public DateMathTimeUnit Interval { get; private set; } public int CompareTo(DateMathTime other) { if (other == null) return 1; if (Math.Abs(_approximateSeconds - other._approximateSeconds) < double.Epsilon) return 0; if (_approximateSeconds < other._approximateSeconds) return -1; return 1; } public bool Equals(DateMathTime other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return Math.Abs(_approximateSeconds - other._approximateSeconds) < double.Epsilon; } public static implicit operator DateMathTime(TimeSpan span) => new DateMathTime(span); public static implicit operator DateMathTime(double milliseconds) => new DateMathTime(milliseconds); public static implicit operator DateMathTime(string expression) => new DateMathTime(expression); private void SetWholeFactorIntervalAndSeconds(double factor, DateMathTimeUnit interval, MidpointRounding rounding) { var fraction = factor; double milliseconds; // if the factor is already a whole number then use it if (TryGetIntegerGreaterThanZero(fraction, out var whole)) { Factor = whole; Interval = interval; switch (interval) { case DateMathTimeUnit.Second: _approximateSeconds = whole; break; case DateMathTimeUnit.Minute: _approximateSeconds = whole * (MillisecondsInAMinute / MillisecondsInASecond); break; case DateMathTimeUnit.Hour: _approximateSeconds = whole * (MillisecondsInAnHour / MillisecondsInASecond); break; case DateMathTimeUnit.Day: _approximateSeconds = whole * (MillisecondsInADay / MillisecondsInASecond); break; case DateMathTimeUnit.Week: _approximateSeconds = whole * (MillisecondsInAWeek / MillisecondsInASecond); break; case DateMathTimeUnit.Month: _approximateSeconds = whole * (MillisecondsInAMonthApproximate / MillisecondsInASecond); break; case DateMathTimeUnit.Year: _approximateSeconds = whole * (MillisecondsInAYearApproximate / MillisecondsInASecond); break; default: throw new ArgumentOutOfRangeException(nameof(interval), interval, null); } return; } switch (interval) { case DateMathTimeUnit.Second: milliseconds = factor * MillisecondsInASecond; break; case DateMathTimeUnit.Minute: milliseconds = factor * MillisecondsInAMinute; break; case DateMathTimeUnit.Hour: milliseconds = factor * MillisecondsInAnHour; break; case DateMathTimeUnit.Day: milliseconds = factor * MillisecondsInADay; break; case DateMathTimeUnit.Week: milliseconds = factor * MillisecondsInAWeek; break; case DateMathTimeUnit.Month: if (TryGetIntegerGreaterThanZero(fraction, out whole)) { Factor = whole; Interval = interval; _approximateSeconds = whole * (MillisecondsInAMonthApproximate / MillisecondsInASecond); return; } milliseconds = factor * MillisecondsInAMonthApproximate; break; case DateMathTimeUnit.Year: if (TryGetIntegerGreaterThanZero(fraction, out whole)) { Factor = whole; Interval = interval; _approximateSeconds = whole * (MillisecondsInAYearApproximate / MillisecondsInASecond); return; } fraction = fraction * MonthsInAYear; if (TryGetIntegerGreaterThanZero(fraction, out whole)) { Factor = whole; Interval = DateMathTimeUnit.Month; _approximateSeconds = whole * (MillisecondsInAMonthApproximate / MillisecondsInASecond); return; } milliseconds = factor * MillisecondsInAYearApproximate; break; default: throw new ArgumentOutOfRangeException(nameof(interval), interval, null); } SetWholeFactorIntervalAndSeconds(milliseconds, rounding); } private void SetWholeFactorIntervalAndSeconds(double milliseconds, MidpointRounding rounding) { double fraction; int whole; if (milliseconds >= MillisecondsInAWeek) { fraction = milliseconds / MillisecondsInAWeek; if (TryGetIntegerGreaterThanZero(fraction, out whole)) { Factor = whole; Interval = DateMathTimeUnit.Week; _approximateSeconds = Factor * (MillisecondsInAWeek / MillisecondsInASecond); return; } } if (milliseconds >= MillisecondsInADay) { fraction = milliseconds / MillisecondsInADay; if (TryGetIntegerGreaterThanZero(fraction, out whole)) { Factor = whole; Interval = DateMathTimeUnit.Day; _approximateSeconds = Factor * (MillisecondsInADay / MillisecondsInASecond); return; } } if (milliseconds >= MillisecondsInAnHour) { fraction = milliseconds / MillisecondsInAnHour; if (TryGetIntegerGreaterThanZero(fraction, out whole)) { Factor = whole; Interval = DateMathTimeUnit.Hour; _approximateSeconds = Factor * (MillisecondsInAnHour / MillisecondsInASecond); return; } } if (milliseconds >= MillisecondsInAMinute) { fraction = milliseconds / MillisecondsInAMinute; if (TryGetIntegerGreaterThanZero(fraction, out whole)) { Factor = whole; Interval = DateMathTimeUnit.Minute; _approximateSeconds = Factor * (MillisecondsInAMinute / MillisecondsInASecond); return; } } if (milliseconds >= MillisecondsInASecond) { fraction = milliseconds / MillisecondsInASecond; if (TryGetIntegerGreaterThanZero(fraction, out whole)) { Factor = whole; Interval = DateMathTimeUnit.Second; _approximateSeconds = Factor; return; } } // round to nearest second, using specified rounding Factor = Convert.ToInt32(Math.Round(milliseconds / MillisecondsInASecond, rounding)); Interval = DateMathTimeUnit.Second; _approximateSeconds = Factor; } private static bool TryGetIntegerGreaterThanZero(double d, out int value) { if (Math.Abs(d % 1) < double.Epsilon) { value = Convert.ToInt32(d); return true; } value = 0; return false; } public static bool operator <(DateMathTime left, DateMathTime right) => left.CompareTo(right) < 0; public static bool operator <=(DateMathTime left, DateMathTime right) => left.CompareTo(right) < 0 || left.Equals(right); public static bool operator >(DateMathTime left, DateMathTime right) => left.CompareTo(right) > 0; public static bool operator >=(DateMathTime left, DateMathTime right) => left.CompareTo(right) > 0 || left.Equals(right); public static bool operator ==(DateMathTime left, DateMathTime right) => left?.Equals(right) ?? ReferenceEquals(right, null); public static bool operator !=(DateMathTime left, DateMathTime right) => !(left == right); public override string ToString() => Factor + Interval.GetStringValue(); public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((DateMathTime)obj); } // ReSharper disable once NonReadonlyMemberInGetHashCode public override int GetHashCode() => _approximateSeconds.GetHashCode(); } internal class DateMathTimeFormatter: IJsonFormatter { public void Serialize(ref JsonWriter writer, DateMathTime value, IJsonFormatterResolver formatterResolver) { if (value is null) writer.WriteNull(); else writer.WriteString(value.ToString()); } public DateMathTime Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) => reader.ReadString(); } }