From c0aecdcdafdb4942d5ce5b865b16a974da0ccbd4 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 3 Jul 2019 12:20:09 +1000 Subject: [PATCH] Include time zone information for Local and Utc DateTimeKind This commit adds the time zone information for a DateTime within a DateMath type, when the DateTimeKind is Local or Utc. This behaviour aligns 7.x with 6.x. Introduce StringBuilderCache for caching a StringBuilder per thread. Use of this can be extended in a later commit. Fixes #3899 --- .../date-math/date-math-expressions.asciidoc | 30 ++++++--- .../DocGenerator/StringExtensions.cs | 2 +- .../DocGenerator/SyntaxNodeExtensions.cs | 7 +- .../Extensions/StringBuilderCache.cs | 64 +++++++++++++++++++ src/Nest/CommonOptions/DateMath/DateMath.cs | 42 ++++++++++-- .../DateMath/DateMathExpressions.doc.cs | 32 +++++++++- 6 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 src/Elasticsearch.Net/Extensions/StringBuilderCache.cs diff --git a/docs/common-options/date-math/date-math-expressions.asciidoc b/docs/common-options/date-math/date-math-expressions.asciidoc index d73020fbd8e..2014b2eb3a8 100644 --- a/docs/common-options/date-math/date-math-expressions.asciidoc +++ b/docs/common-options/date-math/date-math-expressions.asciidoc @@ -83,14 +83,28 @@ anchor will be an actual `DateTime`, even after a serialization/deserialization [source,csharp] ---- var date = new DateTime(2015, 05, 05); -Expect("2015-05-05T00:00:00") - .WhenSerializing(date) - .AssertSubject(dateMath => ((IDateMath)dateMath) - .Anchor.Match( - d => d.Should().Be(date), - s => s.Should().BeNull() - ) - ); +---- + +will serialize to + +[source,javascript] +---- +"2015-05-05T00:00:00" +---- + +When the `DateTime` is local or UTC, the time zone information is included. +For example, for a UTC `DateTime` + +[source,csharp] +---- +var utcDate = new DateTime(2015, 05, 05, 0, 0, 0, DateTimeKind.Utc); +---- + +will serialize to + +[source,javascript] +---- +"2015-05-05T00:00:00Z" ---- ==== Complex expressions diff --git a/src/CodeGeneration/DocGenerator/StringExtensions.cs b/src/CodeGeneration/DocGenerator/StringExtensions.cs index 0f61e90ff8a..0e461d1ffff 100644 --- a/src/CodeGeneration/DocGenerator/StringExtensions.cs +++ b/src/CodeGeneration/DocGenerator/StringExtensions.cs @@ -213,7 +213,7 @@ public static string RemoveNumberOfLeadingTabsOrSpacesAfterNewline(this string i public static string[] SplitOnNewLines(this string input, StringSplitOptions options) => input.Split(new[] { "\r\n", "\n" }, options); - public static bool TryGetJsonForAnonymousType(this string anonymousTypeString, out string json) + public static bool TryGetJsonForExpressionSyntax(this string anonymousTypeString, out string json) { json = null; diff --git a/src/CodeGeneration/DocGenerator/SyntaxNodeExtensions.cs b/src/CodeGeneration/DocGenerator/SyntaxNodeExtensions.cs index e57c2bf3ea6..28316f49a48 100644 --- a/src/CodeGeneration/DocGenerator/SyntaxNodeExtensions.cs +++ b/src/CodeGeneration/DocGenerator/SyntaxNodeExtensions.cs @@ -72,11 +72,10 @@ public static bool TryGetJsonForSyntaxNode(this SyntaxNode node, out string json json = null; // find the first anonymous object or new object expression - var creationExpressionSyntax = node.DescendantNodes() - .FirstOrDefault(n => n is AnonymousObjectCreationExpressionSyntax || n is ObjectCreationExpressionSyntax); + var syntax = node.DescendantNodes() + .FirstOrDefault(n => n is AnonymousObjectCreationExpressionSyntax || n is ObjectCreationExpressionSyntax || n is LiteralExpressionSyntax); - return creationExpressionSyntax != null && - creationExpressionSyntax.ToFullString().TryGetJsonForAnonymousType(out json); + return syntax != null && syntax.ToFullString().TryGetJsonForExpressionSyntax(out json); } /// diff --git a/src/Elasticsearch.Net/Extensions/StringBuilderCache.cs b/src/Elasticsearch.Net/Extensions/StringBuilderCache.cs new file mode 100644 index 00000000000..873dbd9f8d0 --- /dev/null +++ b/src/Elasticsearch.Net/Extensions/StringBuilderCache.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; + +namespace Elasticsearch.Net.Extensions +{ + /// Provide a cached reusable instance of stringbuilder per thread. + internal static class StringBuilderCache + { + private const int DefaultCapacity = 16; // == StringBuilder.DefaultCapacity + + // The value 360 was chosen in discussion with performance experts as a compromise between using + // as little memory per thread as possible and still covering a large part of short-lived + // StringBuilder creations on the startup path of VS designers. + private const int MaxBuilderSize = 360; + + // WARNING: We allow diagnostic tools to directly inspect this member (t_cachedInstance). + // See https://github.com/dotnet/corert/blob/master/Documentation/design-docs/diagnostics/diagnostics-tools-contract.md for more details. + // Please do not change the type, the name, or the semantic usage of this member without understanding the implication for tools. + // Get in touch with the diagnostics team if you have questions. + [ThreadStatic] + private static StringBuilder _cachedInstance; + + /// Get a StringBuilder for the specified capacity. + /// If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied. + public static StringBuilder Acquire(int capacity = DefaultCapacity) + { + if (capacity <= MaxBuilderSize) + { + var sb = _cachedInstance; + if (sb != null) + { + // Avoid stringbuilder block fragmentation by getting a new StringBuilder + // when the requested size is larger than the current capacity + if (capacity <= sb.Capacity) + { + _cachedInstance = null; + sb.Clear(); + return sb; + } + } + } + + return new StringBuilder(capacity); + } + + /// Place the specified builder in the cache if it is not too big. + public static void Release(StringBuilder sb) + { + if (sb.Capacity <= MaxBuilderSize) _cachedInstance = sb; + } + + /// ToString() the stringbuilder, Release it to the cache, and return the resulting string. + public static string GetStringAndRelease(StringBuilder sb) + { + var result = sb.ToString(); + Release(sb); + return result; + } + } +} diff --git a/src/Nest/CommonOptions/DateMath/DateMath.cs b/src/Nest/CommonOptions/DateMath/DateMath.cs index ee32d7d0b8e..c08436564d5 100644 --- a/src/Nest/CommonOptions/DateMath/DateMath.cs +++ b/src/Nest/CommonOptions/DateMath/DateMath.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text; using System.Text.RegularExpressions; using Elasticsearch.Net.Extensions; @@ -109,21 +110,50 @@ public override string ToString() } /// - /// Formats a to have a minimum of 3 decimal places if there - /// are sub second values + /// Formats a to have a minimum of 3 decimal places if there are sub second values /// - /// Fixes bug in Elasticsearch: https://github.com/elastic/elasticsearch/pull/41871 private static string ToMinThreeDecimalPlaces(DateTime dateTime) { - var format = dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFF"); + 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; - return $"{format}{new string('0', diff)}"; + for (int i = 0; i < diff; i++) + builder.Append('0'); } - return format; + 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))); } } diff --git a/src/Tests/Tests/CommonOptions/DateMath/DateMathExpressions.doc.cs b/src/Tests/Tests/CommonOptions/DateMath/DateMathExpressions.doc.cs index 144e06d54b7..3d039b4fe36 100644 --- a/src/Tests/Tests/CommonOptions/DateMath/DateMathExpressions.doc.cs +++ b/src/Tests/Tests/CommonOptions/DateMath/DateMathExpressions.doc.cs @@ -61,7 +61,15 @@ [U] public void SimpleExpressions() * anchor will be an actual `DateTime`, even after a serialization/deserialization round trip */ var date = new DateTime(2015, 05, 05); - Expect("2015-05-05T00:00:00") + + /** + * will serialize to + */ + //json + var expected = "2015-05-05T00:00:00"; + + // hide + Expect(expected) .WhenSerializing(date) .AssertSubject(dateMath => ((IDateMath)dateMath) .Anchor.Match( @@ -69,6 +77,28 @@ [U] public void SimpleExpressions() s => s.Should().BeNull() ) ); + + /** + * When the `DateTime` is local or UTC, the time zone information is included. + * For example, for a UTC `DateTime` + */ + var utcDate = new DateTime(2015, 05, 05, 0, 0, 0, DateTimeKind.Utc); + + /** + * will serialize to + */ + //json + expected = "2015-05-05T00:00:00Z"; + + // hide + Expect(expected) + .WhenSerializing(utcDate) + .AssertSubject(dateMath => ((IDateMath)dateMath) + .Anchor.Match( + d => d.Should().Be(utcDate), + s => s.Should().BeNull() + ) + ); } [U] public void ComplexExpressions()