This is an automated email from the ASF dual-hosted git repository.

nightowl888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucenenet.git


The following commit(s) were added to refs/heads/master by this push:
     new 5864780  BUG: Lucene.Net.QueryParser.Flexible.Standard: Fixed calendar 
and time zone handling on .NET Core (#551)
5864780 is described below

commit 5864780739067f03f8913db9d6807de3a88234f3
Author: Shad Storhaug <[email protected]>
AuthorDate: Fri Dec 3 08:56:03 2021 +0700

    BUG: Lucene.Net.QueryParser.Flexible.Standard: Fixed calendar and time zone 
handling on .NET Core (#551)
    
    * BUG: Lucene.Net.QueryParser.Flexible.Standard.Config.NumberDateFormat: 
Fixed support for non-Gregorian calendars, which were causing random failures 
in Lucene.Net.QueryParser.Flexible.Standard.TestNumericQueryParser.
    
    * Lucene.Net.Tests.QueryParser.Flexible.Standard.TestNumericQueryParser: 
Added bounds checks for random date generation to ensure they are within the 
range of the test culture's calendar.
    
    * BREAKING: Lucene.Net.Util.NumberFormat, 
Lucene.Net.QueryParser.Flexible.Standard.Config.NumberDateFormat: Widened 
constructors to use IFormatProvider instead of CultureInfo and changed Culture 
property to FormatProvider. Added support for time zones to NumberDateFormat.
    
    * Lucene.Net.QueryParser.Flexible.Standard.Config: Added test for 
TestNumberDateFormat
    
    * Lucene.Net.QueryParsers.Flexible.Standard.Config.TestNumberDateFormat: 
Write out formatter output for debug purposes.
    
    * Lucene.Net.QueryParsers.Flexible.Standard: Use DateTimeOffset to simplify 
time zone conversions.
    
    * Lucene.Net.QueryParsers.Flexible.Standard.Config.NumberDateFormat: Fixed 
const initialization order
    
    * Lucene.Net.QueryParsers.Flexible.Standard.Config.NumberDateFormat: Use 
TimeZoneInfo during parsing and when selecting random dates
    
    * BUG: 
Lucene.Net.QueryParsers.Flexible.Standard.TestNumericQueryParser::BeforeClass():
 Corrected date format string to explicitly specify era and to include minutes 
in the time zone offset, which was preventing round-tripping from working 
correctly.
    
    * Lucene.Net.QueryParsers.Flexible.Standard.Config.NumberDateFormat: Use 
full time zone (including minutes) for FULL time
    
    * Lucene.Net.QueryParsers.Flexible.Standard.Config.NumberDateFormat: Added 
support for converting unspecified time zones to the specified time zone + 
tests.
    
    * Lucene.Net.QueryParser.Flexible.Standard.TestNumericQueryParser: Fixed 
nullable warnings
---
 build/Dependencies.props                           |   1 +
 .../Flexible/Standard/Config/NumberDateFormat.cs   | 146 ++++++++++++++++-----
 .../Flexible/Standard/TestNumericQueryParser.cs    | 145 +++++++++++++-------
 .../Lucene.Net.Tests.QueryParser.csproj            |   4 +
 .../Standard/Config/TestNumberDateFormat.cs        | 143 ++++++++++++++++++++
 .../Support/TestApiConsistency.cs                  |   2 +-
 src/Lucene.Net/Support/Util/NumberFormat.cs        |  28 ++--
 7 files changed, 377 insertions(+), 92 deletions(-)

diff --git a/build/Dependencies.props b/build/Dependencies.props
index eac09c6..90c8ce1 100644
--- a/build/Dependencies.props
+++ b/build/Dependencies.props
@@ -82,6 +82,7 @@
     
<SystemSecurityCryptographyXmlPackageVersion>4.7.0</SystemSecurityCryptographyXmlPackageVersion>
     
<SystemTextEncodingCodePagesPackageVersion>4.3.0</SystemTextEncodingCodePagesPackageVersion>
     <SystemTextEncodingCodePagesPackageVersion Condition=" 
'$(TargetFramework)' == 'net461' 
">5.0.0</SystemTextEncodingCodePagesPackageVersion>
+    <TimeZoneConverterPackageVersion>3.5.0</TimeZoneConverterPackageVersion>
     <XUnitPackageVersion>2.3.1</XUnitPackageVersion>
     
<XUnitRunnerVisualStudioPackageVersion>$(XUnitPackageVersion)</XUnitRunnerVisualStudioPackageVersion>
   </PropertyGroup>
diff --git 
a/src/Lucene.Net.QueryParser/Flexible/Standard/Config/NumberDateFormat.cs 
b/src/Lucene.Net.QueryParser/Flexible/Standard/Config/NumberDateFormat.cs
index 6f14c18..1638478 100644
--- a/src/Lucene.Net.QueryParser/Flexible/Standard/Config/NumberDateFormat.cs
+++ b/src/Lucene.Net.QueryParser/Flexible/Standard/Config/NumberDateFormat.cs
@@ -1,6 +1,7 @@
 using Lucene.Net.Util;
 using System;
 using System.Globalization;
+#nullable enable
 
 namespace Lucene.Net.QueryParsers.Flexible.Standard.Config
 {
@@ -42,32 +43,32 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard.Config
     {
         //private static readonly long serialVersionUID = 964823936071308283L;
 
-        // The .NET ticks representing January 1, 1970 0:00:00 GMT, also known 
as the "epoch".
-        public const long EPOCH = 621355968000000000;
-
-        private string dateFormat;
+        private string? dateFormat;
         private readonly DateFormat dateStyle;
         private readonly DateFormat timeStyle;
         private TimeZoneInfo timeZone = TimeZoneInfo.Local;
 
         /// <summary>
         /// Constructs a <see cref="NumberDateFormat"/> object using the given 
<paramref name="dateFormat"/>
-        /// and <paramref name="locale"/>.
+        /// and <paramref name="formatProvider"/>.
         /// </summary>
-        /// <param name="dateFormat">Date format used to parse and format 
dates</param>
-        /// <param name="locale"></param>
-        public NumberDateFormat(string dateFormat, CultureInfo locale)
-            : base(locale)
+        /// <param name="dateFormat">Date format used to parse and format 
dates.</param>
+        /// <param name="formatProvider">An object that supplies 
culture-specific formatting information.</param>
+        public NumberDateFormat(string? dateFormat, IFormatProvider? 
formatProvider)
+            : base(formatProvider)
         {
             this.dateFormat = dateFormat;
         }
 
         /// <summary>
         /// Constructs a <see cref="NumberDateFormat"/> object using the given 
<paramref name="dateStyle"/>,
-        /// <paramref name="timeStyle"/>, and <paramref name="culture"/>.
+        /// <paramref name="timeStyle"/>, and <paramref 
name="formatProvider"/>.
         /// </summary>
-        public NumberDateFormat(DateFormat dateStyle, DateFormat timeStyle, 
CultureInfo culture)
-            : base(culture)
+        /// <param name="dateStyle"></param>
+        /// <param name="timeStyle"></param>
+        /// <param name="formatProvider">An object that supplies 
culture-specific formatting information.</param>
+        public NumberDateFormat(DateFormat dateStyle, DateFormat timeStyle, 
IFormatProvider? formatProvider)
+            : base(formatProvider)
         {
             this.dateStyle = dateStyle;
             this.timeStyle = timeStyle;
@@ -81,26 +82,34 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard.Config
 
         public override string Format(double number)
         {
-            return new 
DateTime(EPOCH).AddMilliseconds(number).ToString(GetDateFormat(), Culture);
+            DateTimeOffset offset = 
DateTimeOffsetUtil.FromUnixTimeMilliseconds(Convert.ToInt64(number));
+            DateTimeOffset timeZoneAdjusted = TimeZoneInfo.ConvertTime(offset, 
TimeZone);
+            return timeZoneAdjusted.ToString(GetDateFormat(), FormatProvider);
         }
 
         public override string Format(long number)
         {
-            return new 
DateTime(EPOCH).AddMilliseconds(number).ToString(GetDateFormat(), Culture);
+            DateTimeOffset offset = 
DateTimeOffsetUtil.FromUnixTimeMilliseconds(Convert.ToInt64(number));
+            DateTimeOffset timeZoneAdjusted = TimeZoneInfo.ConvertTime(offset, 
TimeZone);
+            return timeZoneAdjusted.ToString(GetDateFormat(), FormatProvider);
         }
 
         public override object Parse(string source)
         {
-            // Try exact format first, if it fails, do a loose DateTime.Parse
-            DateTime d;
-            d = DateTime.ParseExact(source, GetDateFormat(), Culture, 
DateTimeStyles.None);
-
-            return (d - new DateTime(EPOCH)).TotalMilliseconds;
+            DateTimeOffset parsedDate = DateTimeOffset.ParseExact(source, 
GetDateFormat(), FormatProvider, DateTimeStyles.None);
+            DateTimeOffset timeZoneAdjusted;
+            if (parsedDate.DateTime.Kind == DateTimeKind.Unspecified)
+                timeZoneAdjusted = new DateTimeOffset(parsedDate.DateTime, 
TimeZoneInfo.ConvertTime(parsedDate.ToUniversalTime(), TimeZone).Offset);
+            else
+                timeZoneAdjusted = TimeZoneInfo.ConvertTime(parsedDate, 
TimeZone);
+            return DateTimeOffsetUtil.ToUnixTimeMilliseconds(timeZoneAdjusted);
         }
 
         public override string Format(object number)
         {
-            return new DateTime(EPOCH).AddMilliseconds(Convert.ToInt64(number, 
CultureInfo.InvariantCulture)).ToString(GetDateFormat(), Culture);
+            DateTimeOffset offset = 
DateTimeOffsetUtil.FromUnixTimeMilliseconds(Convert.ToInt64(number, 
FormatProvider));
+            DateTimeOffset timeZoneAdjusted = TimeZoneInfo.ConvertTime(offset, 
TimeZone);
+            return timeZoneAdjusted.ToString(GetDateFormat(), FormatProvider);
         }
 
         public void SetDateFormat(string dateFormat)
@@ -117,49 +126,122 @@ namespace 
Lucene.Net.QueryParsers.Flexible.Standard.Config
         {
             if (dateFormat != null) return dateFormat;
 
-            return GetDateFormat(this.dateStyle, this.timeStyle, Culture);
+            return GetDateFormat(this.dateStyle, this.timeStyle, 
FormatProvider);
         }
 
-        public static string GetDateFormat(DateFormat dateStyle, DateFormat 
timeStyle, CultureInfo culture)
+        public static string GetDateFormat(DateFormat dateStyle, DateFormat 
timeStyle, IFormatProvider? provider)
         {
             string datePattern = "", timePattern = "";
+            DateTimeFormatInfo dateTimeFormat = (provider ?? 
DateTimeFormatInfo.CurrentInfo)
+                .GetFormat(typeof(DateTimeFormatInfo)) as DateTimeFormatInfo 
?? DateTimeFormatInfo.CurrentInfo;
 
             switch (dateStyle)
             {
                 case DateFormat.SHORT:
-                    datePattern = culture.DateTimeFormat.ShortDatePattern;
+                    datePattern = dateTimeFormat.ShortDatePattern;
                     break;
                 case DateFormat.MEDIUM:
-                    datePattern = culture.DateTimeFormat.LongDatePattern
-                        .Replace("dddd,", "").Replace(", dddd", "") // Remove 
the day of the week
+                    datePattern = dateTimeFormat.LongDatePattern
+                        .Replace("dddd, ", "").Replace(", dddd", "") // Remove 
the day of the week
                         .Replace("MMMM", "MMM"); // Replace month with 
abbreviated month
                     break;
                 case DateFormat.LONG:
-                    datePattern = culture.DateTimeFormat.LongDatePattern
-                        .Replace("dddd,", "").Replace(", dddd", ""); // Remove 
the day of the week
+                    datePattern = dateTimeFormat.LongDatePattern
+                        .Replace("dddd, ", "").Replace(", dddd", ""); // 
Remove the day of the week
                     break;
                 case DateFormat.FULL:
-                    datePattern = culture.DateTimeFormat.LongDatePattern;
+                    datePattern = dateTimeFormat.LongDatePattern;
                     break;
             }
 
             switch (timeStyle)
             {
                 case DateFormat.SHORT:
-                    timePattern = culture.DateTimeFormat.ShortTimePattern;
+                    timePattern = dateTimeFormat.ShortTimePattern;
                     break;
                 case DateFormat.MEDIUM:
-                    timePattern = culture.DateTimeFormat.LongTimePattern;
+                    timePattern = dateTimeFormat.LongTimePattern;
                     break;
                 case DateFormat.LONG:
-                    timePattern = 
culture.DateTimeFormat.LongTimePattern.Replace("z", "").Trim() + " z";
+                    timePattern = dateTimeFormat.LongTimePattern.Replace("z", 
"").Trim() + " z";
                     break;
                 case DateFormat.FULL:
-                    timePattern = 
culture.DateTimeFormat.LongTimePattern.Replace("z", "").Trim() + " z"; // 
LUCENENET TODO: Time zone info not being added to match behavior of Java, but 
Java doc is unclear on what the difference is between this and LONG
+                    timePattern = dateTimeFormat.LongTimePattern.Replace("z", 
"").Trim() + " zzz";
                     break;
             }
 
             return string.Concat(datePattern, " ", timePattern);
         }
     }
+
+    // Source: 
https://github.com/dotnet/runtime/blob/af4efb1936b407ca5f4576e81484cf5687b79a26/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs
+    internal static class DateTimeOffsetUtil
+    {
+        /// <summary>
+        /// The .NET ticks representing January 1, 1970 0:00:00, also known as 
the "epoch".
+        /// </summary>
+        private const long UnixEpochTicks = 621355968000000000L;
+
+        private const long UnixEpochMilliseconds = UnixEpochTicks / 
TimeSpan.TicksPerMillisecond; // 62,135,596,800,000
+
+        public const long MinMilliseconds = /*DateTime.*/MinTicks / 
TimeSpan.TicksPerMillisecond - UnixEpochMilliseconds;
+        public const long MaxMilliseconds = /*DateTime.*/MaxTicks / 
TimeSpan.TicksPerMillisecond - UnixEpochMilliseconds;
+
+        // From System.DateTime
+
+        // Number of 100ns ticks per time unit
+        private const long TicksPerMillisecond = 10000;
+        private const long TicksPerSecond = TicksPerMillisecond * 1000;
+        private const long TicksPerMinute = TicksPerSecond * 60;
+        private const long TicksPerHour = TicksPerMinute * 60;
+        private const long TicksPerDay = TicksPerHour * 24;
+
+        // Number of days in a non-leap year
+        private const int DaysPerYear = 365;
+        // Number of days in 4 years
+        private const int DaysPer4Years = DaysPerYear * 4 + 1;       // 1461
+        // Number of days in 100 years
+        private const int DaysPer100Years = DaysPer4Years * 25 - 1;  // 36524
+        // Number of days in 400 years
+        private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097
+
+        // Number of days from 1/1/0001 to 12/31/9999
+        private const int DaysTo10000 = DaysPer400Years * 25 - 366;  // 3652059
+
+        internal const long MinTicks = 0;
+        internal const long MaxTicks = DaysTo10000 * TicksPerDay - 1;
+
+
+        public static long GetTicksFromUnixTimeMilliseconds(long milliseconds)
+        {
+            if (milliseconds < MinMilliseconds || milliseconds > 
MaxMilliseconds)
+            {
+                throw new ArgumentOutOfRangeException(nameof(milliseconds),
+                    string.Format("Valid values are between {0} and {1}, 
inclusive.", MinMilliseconds, MaxMilliseconds));
+            }
+
+            long ticks = milliseconds * TimeSpan.TicksPerMillisecond + 
UnixEpochTicks;
+            return ticks;
+        }
+
+        public static DateTimeOffset FromUnixTimeMilliseconds(long 
milliseconds)
+        {
+            if (milliseconds < MinMilliseconds || milliseconds > 
MaxMilliseconds)
+            {
+                throw new ArgumentOutOfRangeException(nameof(milliseconds),
+                    string.Format("Valid values are between {0} and {1}, 
inclusive.", MinMilliseconds, MaxMilliseconds));
+            }
+
+            long ticks = milliseconds * TimeSpan.TicksPerMillisecond + 
UnixEpochTicks;
+            return new DateTimeOffset(ticks, TimeSpan.Zero);
+        }
+
+        public static long ToUnixTimeMilliseconds(DateTimeOffset offset)
+        {
+            // Truncate sub-millisecond precision before offsetting by the 
Unix Epoch to avoid
+            // the last digit being off by one for dates that result in 
negative Unix times
+            long milliseconds = offset.UtcDateTime.Ticks / 
TimeSpan.TicksPerMillisecond;
+            return milliseconds - UnixEpochMilliseconds;
+        }
+    }
 }
diff --git 
a/src/Lucene.Net.Tests.QueryParser/Flexible/Standard/TestNumericQueryParser.cs 
b/src/Lucene.Net.Tests.QueryParser/Flexible/Standard/TestNumericQueryParser.cs
index db78f57..dc980c1 100644
--- 
a/src/Lucene.Net.Tests.QueryParser/Flexible/Standard/TestNumericQueryParser.cs
+++ 
b/src/Lucene.Net.Tests.QueryParser/Flexible/Standard/TestNumericQueryParser.cs
@@ -16,6 +16,7 @@ using System.Globalization;
 using System.Text;
 using Console = Lucene.Net.Util.SystemConsole;
 using JCG = J2N.Collections.Generic;
+#nullable enable
 
 namespace Lucene.Net.QueryParsers.Flexible.Standard
 {
@@ -48,33 +49,86 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
 
         private readonly static int PRECISION_STEP = 8;
         private readonly static String FIELD_NAME = "field";
-        private static CultureInfo LOCALE;
-        private static TimeZoneInfo TIMEZONE;
-        private static IDictionary<String, /*Number*/ object> 
RANDOM_NUMBER_MAP;
+        private static CultureInfo? LOCALE;
+        private static TimeZoneInfo? TIMEZONE;
+        private static IDictionary<String, /*Number*/ object>? 
RANDOM_NUMBER_MAP;
         private readonly static IEscapeQuerySyntax ESCAPER = new 
Standard.Parser.EscapeQuerySyntax();
         private readonly static String DATE_FIELD_NAME = "date";
         private static DateFormat DATE_STYLE;
         private static DateFormat TIME_STYLE;
 
-        private static Analyzer ANALYZER;
+        private static Analyzer? ANALYZER;
 
-        private static NumberFormat NUMBER_FORMAT;
+        private static NumberFormat? NUMBER_FORMAT;
         
 
-        private static StandardQueryParser qp;
+        private static StandardQueryParser? qp;
 
-        private static NumberDateFormat DATE_FORMAT;
+        private static NumberDateFormat? DATE_FORMAT;
 
-        private static Directory directory = null;
-        private static IndexReader reader = null;
-        private static IndexSearcher searcher = null;
+        private static Directory? directory = null;
+        private static IndexReader? reader = null;
+        private static IndexSearcher? searcher = null;
 
-        private static bool checkDateFormatSanity(/*DateFormat*/string 
dateFormat, long date)
+        private static bool checkDateFormatSanity(NumberDateFormat dateFormat, 
long date, TimeZoneInfo timeZone)
         {
-            return DateTime.TryParseExact(new 
DateTime(NumberDateFormat.EPOCH).AddMilliseconds(date).ToString(dateFormat),
-                dateFormat, CultureInfo.CurrentCulture, 
DateTimeStyles.RoundtripKind, out DateTime _);
+            IFormatProvider provider = dateFormat.FormatProvider ?? 
CultureInfo.CurrentCulture;
+
+            if (IsOutOfBounds(date, provider))
+                return false;
+
+            string format = dateFormat.GetDateFormat();
+            DateTimeOffset offset = 
DateTimeOffsetUtil.FromUnixTimeMilliseconds(Convert.ToInt64(date));
+            offset = TimeZoneInfo.ConvertTime(offset, timeZone);
+            string formattedDate = offset.ToString(format, provider);
+
+            return DateTimeOffset.TryParseExact(formattedDate, format, 
provider, DateTimeStyles.None, out DateTimeOffset _);
+        }
+
+        // LUCENENET specific bounds check
+        // We need to be sure that the date is within the range of the current 
calendar, or we will get
+        // an ArgumentOutOfRangeException when attempting to materialize it.
+        private static bool IsOutOfBounds(double date, IFormatProvider 
provider)
+        {
+            Calendar calendar = GetCalendar(provider);
+
+            if (date < DateTimeOffsetUtil.MinMilliseconds || date > 
DateTimeOffsetUtil.MaxMilliseconds)
+                return false;
+
+            // We can't convert to a DateTimeOffset because it will do the 
calendar check and throw ArgumentOutOfRangeException
+            // before we can check the range.
+            long newDateTicks = 
DateTimeOffsetUtil.GetTicksFromUnixTimeMilliseconds(Convert.ToInt64(date));
+            return newDateTicks < calendar.MinSupportedDateTime.Ticks || 
newDateTicks > calendar.MaxSupportedDateTime.Ticks;
         }
 
+        // LUCENENET specific bounds check
+        private static bool IsOutOfBoundsOrZero(double absNumber, 
IFormatProvider provider)
+        {
+            return absNumber == 0 || IsOutOfBounds(absNumber, provider) || 
IsOutOfBounds(-absNumber, provider);
+        }
+
+        /// <summary>
+        /// Returns the <see cref="Calendar"/> from the specified <paramref 
name="provider"/>.
+        /// </summary>
+        /// <param name="provider">
+        /// The provider to use to format the value.
+        /// <para/>
+        /// -or-
+        /// <para/>
+        /// A null reference (Nothing in Visual Basic) to obtain the numeric 
format information from the locale setting of the current thread.
+        /// </param>
+        /// <returns>The <see cref="Calendar"/> instance.</returns>
+        /// <exception cref="NotSupportedException">The supplied <paramref 
name="provider"/> returned <c>null</c> for the requested type <see 
cref="DateTimeFormatInfo"/>.</exception>
+        internal static Calendar GetCalendar(IFormatProvider? provider)
+        {
+            DateTimeFormatInfo? dateTimeFormat = (provider ?? 
DateTimeFormatInfo.CurrentInfo).GetFormat(typeof(DateTimeFormatInfo)) as 
DateTimeFormatInfo;
+            if (dateTimeFormat is null)
+                throw new NotSupportedException($"The specified format 
provider did not return a '{typeof(DateTimeFormatInfo).FullName}' instance from 
IFormatProvider.GetFormat(System.Type).");
+
+            return dateTimeFormat.Calendar;
+        }
+
+
         [OneTimeSetUp]
         public override void BeforeClass()
         {
@@ -87,7 +141,7 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
             IDictionary<string, /*Number*/object> randomNumberMap = new 
JCG.Dictionary<string, object>();
 
             /*SimpleDateFormat*/
-            string dateFormat;
+            //string dateFormat;
             long randomDate;
             bool dateFormatSanityCheckPass;
             int count = 0;
@@ -123,10 +177,10 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
 
                 // not all date patterns includes era, full year, timezone and 
second,
                 // so we add them here
-                DATE_FORMAT.SetDateFormat(DATE_FORMAT.GetDateFormat() + " g s 
z yyyy");
+                DATE_FORMAT.SetDateFormat(DATE_FORMAT.GetDateFormat() + " %g s 
zzz yyyy");
 
 
-                dateFormat = DATE_FORMAT.GetDateFormat();
+                //dateFormat = DATE_FORMAT.GetDateFormat();
 
                 do
                 {
@@ -143,12 +197,12 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
                     randomDate = Math.Abs(randomDate);
                 } while (randomDate == 0L);
 
-                dateFormatSanityCheckPass &= checkDateFormatSanity(dateFormat, 
randomDate);
+                dateFormatSanityCheckPass &= 
checkDateFormatSanity(DATE_FORMAT, randomDate, TIMEZONE);
 
-                dateFormatSanityCheckPass &= checkDateFormatSanity(dateFormat, 
0);
+                dateFormatSanityCheckPass &= 
checkDateFormatSanity(DATE_FORMAT, 0, TIMEZONE);
 
-                dateFormatSanityCheckPass &= checkDateFormatSanity(dateFormat,
-                          -randomDate);
+                dateFormatSanityCheckPass &= checkDateFormatSanity(DATE_FORMAT,
+                          -randomDate, TIMEZONE);
 
                 count++;
             } while (!dateFormatSanityCheckPass);
@@ -166,16 +220,17 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
             int randomInt;
             float randomFloat;
 
-            while ((randomLong = 
Convert.ToInt64(NormalizeNumber(Math.Abs(Random.nextLong()))
-                )) == 0L)
+            while (IsOutOfBoundsOrZero((randomLong = 
Convert.ToInt64(NormalizeNumber(Math.Abs(Random.nextLong()))
+                )), LOCALE))
                 ;
-            while ((randomDouble = 
Convert.ToDouble(NormalizeNumber(Math.Abs(Random.NextDouble()))
-                )) == 0.0)
+            while (IsOutOfBoundsOrZero((randomDouble = 
Convert.ToDouble(NormalizeNumber(Math.Abs(Random.NextDouble()))
+                )), LOCALE))
                 ;
-            while ((randomFloat = 
Convert.ToSingle(NormalizeNumber(Math.Abs(Random.nextFloat()))
-                )) == 0.0f)
+            while (IsOutOfBoundsOrZero((randomFloat = 
Convert.ToSingle(NormalizeNumber(Math.Abs(Random.nextFloat()))
+                )), LOCALE))
                 ;
-            while ((randomInt = 
Convert.ToInt32(NormalizeNumber(Math.Abs(Random.nextInt())))) == 0)
+            while (IsOutOfBoundsOrZero((randomInt = 
Convert.ToInt32(NormalizeNumber(Math.Abs(Random.nextInt()))
+                )), LOCALE))
                 ;
 
             randomNumberMap.Put(NumericType.INT64.ToString(), randomLong);
@@ -230,7 +285,7 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
                         break;
                     default:
                         fail();
-                        field = null;
+                        field = null!;
                         break;
                 }
                 numericFieldMap.Put(type.ToString(), field);
@@ -259,7 +314,7 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
 
         }
 
-        private static /*Number*/ object GetNumberType(NumberType? numberType, 
String fieldName)
+        private static /*Number*/ object? GetNumberType(NumberType? 
numberType, String fieldName)
         {
 
             if (numberType == null)
@@ -271,11 +326,11 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
             {
 
                 case NumberType.POSITIVE:
-                    return RANDOM_NUMBER_MAP[fieldName];
+                    return RANDOM_NUMBER_MAP![fieldName];
 
                 case NumberType.NEGATIVE:
                     /*Number*/
-                    object number = RANDOM_NUMBER_MAP[fieldName];
+                    object number = RANDOM_NUMBER_MAP![fieldName];
 
                     if (NumericType.INT64.ToString().Equals(fieldName, 
StringComparison.Ordinal)
                         || DATE_FIELD_NAME.Equals(fieldName, 
StringComparison.Ordinal))
@@ -318,7 +373,7 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
         {
 
             /*Number*/
-            object number = GetNumberType(numberType, NumericType.DOUBLE
+            object? number = GetNumberType(numberType, NumericType.DOUBLE
                 .ToString());
             
numericFieldMap[NumericType.DOUBLE.ToString()].SetDoubleValue(Convert.ToDouble(
                 number));
@@ -458,9 +513,9 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
             }
 
             /*Number*/
-            object lowerDateNumber = GetNumberType(lowerType, DATE_FIELD_NAME);
+            object? lowerDateNumber = GetNumberType(lowerType, 
DATE_FIELD_NAME);
             /*Number*/
-            object upperDateNumber = GetNumberType(upperType, DATE_FIELD_NAME);
+            object? upperDateNumber = GetNumberType(upperType, 
DATE_FIELD_NAME);
             String lowerDateStr;
             String upperDateStr;
 
@@ -471,7 +526,7 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
                 //    EscapeQuerySyntax.Type.STRING).toString();
 
                 lowerDateStr = ESCAPER.Escape(
-                            
DATE_FORMAT.Format(Convert.ToInt64(lowerDateNumber, 
CultureInfo.InvariantCulture)),
+                            
DATE_FORMAT!.Format(Convert.ToInt64(lowerDateNumber, 
CultureInfo.InvariantCulture)),
                             LOCALE,
                             EscapeQuerySyntaxType.STRING).toString();
             }
@@ -487,7 +542,7 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
                 //      EscapeQuerySyntax.Type.STRING).toString();
 
                 upperDateStr = ESCAPER.Escape(
-                                
DATE_FORMAT.Format(Convert.ToInt64(upperDateNumber, 
CultureInfo.InvariantCulture)),
+                                
DATE_FORMAT!.Format(Convert.ToInt64(upperDateNumber, 
CultureInfo.InvariantCulture)),
                                 LOCALE,
                                 EscapeQuerySyntaxType.STRING).toString();
             }
@@ -528,7 +583,7 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
             //        .longValue())), LOCALE, 
EscapeQuerySyntax.Type.STRING).toString();
 
             string boundDateStr = ESCAPER.Escape(
-                                
DATE_FORMAT.Format(Convert.ToInt64(GetNumberType(boundType, DATE_FIELD_NAME))),
+                                
DATE_FORMAT!.Format(Convert.ToInt64(GetNumberType(boundType, DATE_FIELD_NAME))),
                                 LOCALE,
                                 EscapeQuerySyntaxType.STRING).toString();
 
@@ -559,7 +614,7 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
             //        .longValue())), LOCALE, 
EscapeQuerySyntax.Type.STRING).toString();
 
             string dateStr = ESCAPER.Escape(
-                                
DATE_FORMAT.Format(Convert.ToInt64(GetNumberType(numberType, DATE_FIELD_NAME))),
+                                
DATE_FORMAT!.Format(Convert.ToInt64(GetNumberType(numberType, 
DATE_FIELD_NAME))),
                                 LOCALE,
                                 EscapeQuerySyntaxType.STRING).toString();
 
@@ -575,9 +630,9 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
         {
             if (Verbose) Console.WriteLine("Parsing: " + queryStr);
 
-            Query query = qp.Parse(queryStr, FIELD_NAME);
+            Query query = qp!.Parse(queryStr, FIELD_NAME);
             if (Verbose) Console.WriteLine("Querying: " + query);
-            TopDocs topDocs = searcher.Search(query, 1000);
+            TopDocs topDocs = searcher!.Search(query, 1000);
 
             String msg = "Query <" + queryStr + "> retrieved " + 
topDocs.TotalHits
                 + " document(s), " + expectedDocCount + " document(s) 
expected.";
@@ -588,24 +643,24 @@ namespace Lucene.Net.QueryParsers.Flexible.Standard
             assertEquals(msg, expectedDocCount, topDocs.TotalHits);
         }
 
-        private static String NumberToString(/*Number*/ object number)
+        private static String NumberToString(/*Number*/ object? number)
         {
-            return number == null ? "*" : 
ESCAPER.Escape(NUMBER_FORMAT.Format(number),
+            return number == null ? "*" : 
ESCAPER.Escape(NUMBER_FORMAT!.Format(number),
                 LOCALE, EscapeQuerySyntaxType.STRING).toString();
         }
 
         private static /*Number*/ object NormalizeNumber(/*Number*/ object 
number)
         {
-            return NUMBER_FORMAT.Parse(NUMBER_FORMAT.Format(number));
+            return NUMBER_FORMAT!.Parse(NUMBER_FORMAT.Format(number));
         }
 
         [OneTimeTearDown]
         public override void AfterClass()
         {
             searcher = null;
-            reader.Dispose();
+            reader?.Dispose();
             reader = null;
-            directory.Dispose();
+            directory?.Dispose();
             directory = null;
             qp = null;
 
diff --git 
a/src/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj 
b/src/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj
index 451bfa7..e198002 100644
--- a/src/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj
+++ b/src/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj
@@ -86,4 +86,8 @@
     <Folder Include="Resources\" />
   </ItemGroup>
 
+  <ItemGroup>
+    <PackageReference Include="TimeZoneConverter" 
Version="$(TimeZoneConverterPackageVersion)" />
+  </ItemGroup>
+
 </Project>
diff --git 
a/src/Lucene.Net.Tests.QueryParser/Support/Flexible/Standard/Config/TestNumberDateFormat.cs
 
b/src/Lucene.Net.Tests.QueryParser/Support/Flexible/Standard/Config/TestNumberDateFormat.cs
new file mode 100644
index 0000000..15d8aff
--- /dev/null
+++ 
b/src/Lucene.Net.Tests.QueryParser/Support/Flexible/Standard/Config/TestNumberDateFormat.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using J2N;
+using Lucene.Net.Attributes;
+using Lucene.Net.Util;
+using TimeZoneConverter;
+using NUnit.Framework;
+using Console = Lucene.Net.Util.SystemConsole;
+
+
+namespace Lucene.Net.QueryParsers.Flexible.Standard.Config
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF 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.
+     */
+
+    [LuceneNetSpecific]
+    public class TestNumberDateFormat : LuceneTestCase
+    {
+        [Test]
+        [LuceneNetSpecific]
+        public void TestTimeZone_PacificTime()
+        {
+            TimeZoneInfo timeZone = TZConvert.GetTimeZoneInfo("Pacific 
Standard Time");
+
+            CultureInfo culture = new CultureInfo("en-US");
+
+            var formatter = new NumberDateFormat(DateFormat.LONG, 
DateFormat.LONG, culture)
+            {
+                TimeZone = timeZone
+            };
+
+            // Convert from Unix epoch to time zone.
+            DateTime dateToParse = 
TimeZoneInfo.ConvertTimeFromUtc(J2N.Time.UnixEpoch, timeZone);
+
+            // Get the difference since the Unix epoch in milliseconds.
+            long dateAsLong = dateToParse.GetMillisecondsSinceUnixEpoch();
+
+            string actual = formatter.Format(dateAsLong);
+
+            Console.WriteLine("Output of formatter.Format():");
+            Console.WriteLine($"\"{actual}\"");
+
+
+            // Make sure time zone is correct in the string for PST, including 
DST.
+            if (timeZone.IsDaylightSavingTime(dateToParse))
+                Assert.IsTrue(Regex.IsMatch(actual, @"\-\s?0?7"));
+            else
+                Assert.IsTrue(Regex.IsMatch(actual, @"\-\s?0?8"));
+
+            // Convert the parsed result back to a long
+            long parsedLong = Convert.ToInt64(formatter.Parse(actual));
+
+            // Make sure round trip results in the same number
+            Assert.AreEqual(dateAsLong, parsedLong);
+        }
+
+        // Verify that we can round-trip and convert to the time zone that is 
set after the parse.
+        [Test]
+        [LuceneNetSpecific]
+        public void TestTimeZone_ShortTimeFormat()
+        {
+            TimeZoneInfo timeZone = TZConvert.GetTimeZoneInfo("Pacific 
Standard Time");
+
+            CultureInfo culture = new CultureInfo("en-US");
+
+            var formatter = new NumberDateFormat(DateFormat.LONG, 
DateFormat.SHORT, culture) // Short time = no time zone info
+            {
+                TimeZone = timeZone
+            };
+
+            // Convert from Unix epoch to time zone.
+            DateTime dateToParse = 
TimeZoneInfo.ConvertTimeFromUtc(J2N.Time.UnixEpoch, timeZone);
+
+            // Get the difference since the Unix epoch in milliseconds.
+            long dateAsLong = dateToParse.GetMillisecondsSinceUnixEpoch();
+
+            string actual = formatter.Format(dateAsLong);
+
+            Console.WriteLine("Output of formatter.Format():");
+            Console.WriteLine($"\"{actual}\"");
+
+
+            // Convert the parsed result back to a long
+            long parsedLong = Convert.ToInt64(formatter.Parse(actual));
+
+            // Make sure round trip results in the same number
+            Assert.AreEqual(dateAsLong, parsedLong);
+        }
+
+        // Verify that we can round-trip and convert to the time zone that is 
set after the parse
+        // in a time zone with different rules prior to 1903. See: 
https://github.com/dotnet/runtime/issues/62247
+        [Test]
+        [LuceneNetSpecific]
+        public void TestTimeZone_ShortTimeFormat_CentralAfricaTime()
+        {
+            TimeZoneInfo timeZone = 
TZConvert.GetTimeZoneInfo("Africa/Gaborone");
+
+            CultureInfo culture = new CultureInfo("en-US");
+
+            var formatter = new NumberDateFormat(DateFormat.LONG, 
DateFormat.MEDIUM, culture) // Medium time = no time zone info, but contains 
seconds
+            {
+                TimeZone = timeZone
+            };
+
+            const long Oct_31_1871_Ticks = 590376582130000000L; // Oct 31, 
1871 05:30:13 UTC
+
+            DateTime dateToParse = TimeZoneInfo.ConvertTime(new 
DateTime(Oct_31_1871_Ticks, DateTimeKind.Utc), timeZone);
+
+            // Get the difference since the Unix epoch in milliseconds.
+            long dateAsLong = dateToParse.GetMillisecondsSinceUnixEpoch();
+
+            string actual = formatter.Format(dateAsLong);
+
+            Console.WriteLine("Output of formatter.Format():");
+            Console.WriteLine($"\"{actual}\"");
+
+            // Convert the parsed result back to a long
+            long parsedLong = Convert.ToInt64(formatter.Parse(actual));
+
+            // Make sure round trip results in the same number
+            Assert.AreEqual(dateAsLong, parsedLong);
+        }
+    }
+}
diff --git a/src/Lucene.Net.Tests.QueryParser/Support/TestApiConsistency.cs 
b/src/Lucene.Net.Tests.QueryParser/Support/TestApiConsistency.cs
index 8565794..18351ab 100644
--- a/src/Lucene.Net.Tests.QueryParser/Support/TestApiConsistency.cs
+++ b/src/Lucene.Net.Tests.QueryParser/Support/TestApiConsistency.cs
@@ -38,7 +38,7 @@ namespace Lucene.Net.QueryParsers
         [TestCase(typeof(Lucene.Net.QueryParsers.Classic.ICharStream))]
         public override void TestPrivateFieldNames(Type typeFromTargetAssembly)
         {
-            base.TestPrivateFieldNames(typeFromTargetAssembly, 
@"Snowball\.Ext\..+Stemmer");
+            base.TestPrivateFieldNames(typeFromTargetAssembly, 
@"DateTimeOffsetUtil");
         }
 
         [Test, LuceneNetSpecific]
diff --git a/src/Lucene.Net/Support/Util/NumberFormat.cs 
b/src/Lucene.Net/Support/Util/NumberFormat.cs
index 80ad26e..8ca01c0 100644
--- a/src/Lucene.Net/Support/Util/NumberFormat.cs
+++ b/src/Lucene.Net/Support/Util/NumberFormat.cs
@@ -33,19 +33,19 @@ namespace Lucene.Net.Util
     // types instead of just the ones that Java supports, as well.
     public class NumberFormat
     {
-        private readonly CultureInfo culture;
+        private readonly IFormatProvider formatProvider;
 
         //private int maximumIntegerDigits;
         //private int minimumIntegerDigits;
         //private int maximumFractionDigits;
         //private int minimumFractionDigits;
 
-        public NumberFormat(CultureInfo culture)
+        public NumberFormat(IFormatProvider formatProvider)
         {
-            this.culture = culture;
+            this.formatProvider = formatProvider;
         }
 
-        protected CultureInfo Culture => culture;
+        public IFormatProvider FormatProvider => formatProvider;
 
         public virtual string Format(object number)
         {
@@ -53,27 +53,27 @@ namespace Lucene.Net.Util
 
             if (number is int i)
             {
-                return i.ToString(format, culture);
+                return i.ToString(format, formatProvider);
             }
             else if (number is long l)
             {
-                return l.ToString(format, culture);
+                return l.ToString(format, formatProvider);
             }
             else if (number is short s)
             {
-                return s.ToString(format, culture);
+                return s.ToString(format, formatProvider);
             }
             else if (number is float f)
             {
-                return f.ToString(format, culture);
+                return f.ToString(format, formatProvider);
             }
             else if (number is double d)
             {
-                return d.ToString(format, culture);
+                return d.ToString(format, formatProvider);
             }
             else if (number is decimal dec)
             {
-                return dec.ToString(format, culture);
+                return dec.ToString(format, formatProvider);
             }
 
             throw new ArgumentException("Cannot format given object as a 
Number");
@@ -82,13 +82,13 @@ namespace Lucene.Net.Util
         public virtual string Format(double number)
         {
             string format = GetNumberFormat();
-            return number.ToString(format, culture);
+            return number.ToString(format, formatProvider);
         }
 
         public virtual string Format(long number)
         {
             string format = GetNumberFormat();
-            return number.ToString(format, culture);
+            return number.ToString(format, formatProvider);
         }
 
         /// <summary>
@@ -104,12 +104,12 @@ namespace Lucene.Net.Util
 
         public virtual /*Number*/ object Parse(string source)
         {
-            return decimal.Parse(source, culture);
+            return decimal.Parse(source, formatProvider);
         }
 
         public override string ToString()
         {
-            return base.ToString() + " - " + GetNumberFormat() + " - " + 
culture.ToString();
+            return base.ToString() + " - " + GetNumberFormat() + " - " + 
formatProvider.ToString();
         }
 
         // LUCENENET TODO: Add additional functionality to edit the 
NumberFormatInfo

Reply via email to