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