FixAntenna/NetCore/Common/DateTimeHelper.cs (249 lines of code) (raw):
// Copyright (c) 2021 EPAM Systems
//
// Licensed 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.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Epam.FixAntenna.NetCore.Common.Logging;
using Epam.FixAntenna.NetCore.Common.Utils;
using Epam.FixAntenna.NetCore.Helpers;
namespace Epam.FixAntenna.NetCore.Common
{
public static class DateTimeHelper
{
private static readonly ILog Log = LogFactory.GetLog(typeof(DateTimeHelper));
public const string GeneralFormat = "yyyy-MM-ddTHH:mm:ss";
public const string MinutesFormat = "yyyy-MM-ddTHH:mm";
public const string MillisecondsFormat = "yyyy-MM-ddTHH:mm:ss.fff";
public const string MicrosecondsFormat = "yyyy-MM-ddTHH:mm:ss.ffffff";
public const string NanosecondsFormat = "yyyy-MM-ddTHH:mm:ss.fffffff00";
private const string TimeZoneZeroSuffix = "Z";
private const string TimeZoneHoursSuffix = "%K";
private const string TimeZoneHoursAndMinutesSuffix = "%K";
public const int TicksPerMicrosecond = 10;
public const int NanosecondsPerTick = 100;
public static readonly TimeSpan UtcOffset = TimeSpan.Zero;
public static readonly TimeSpan LocalZoneOffset = TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow);
private static readonly Calendar GregorianCalendar = new GregorianCalendar();
private static readonly double SwPerTick = (double)TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
private static DateTime _baseNow = DateTime.UtcNow;
private static double _baseTimestamp = Stopwatch.GetTimestamp();
private static readonly long SetBaseInterval = TimeSpan.FromSeconds(10).Ticks; // sync to system time each ten seconds
private static readonly object Lock = new object();
/// <summary>
/// Gets number of ticks representing current UTC time. One tick is 100 ns, equal to DateTime.Tick unit.
/// </summary>
public static long CurrentTicks
{
get
{
var endTime = Stopwatch.GetTimestamp();
var delta = (endTime - _baseTimestamp) * SwPerTick;
if (delta < SetBaseInterval)
{
return _baseNow.Ticks + (long)delta;
}
lock (Lock)
{
_baseTimestamp = Stopwatch.GetTimestamp();
_baseNow = DateTime.UtcNow;
}
return _baseNow.Ticks;
}
}
/// <summary>
/// Gets number of seconds representing current UTC time.
/// </summary>
public static long CurrentSeconds => CurrentTicks / TimeSpan.TicksPerSecond;
/// <summary>
/// Gets number of milliseconds representing current UTC time.
/// </summary>
public static long CurrentMilliseconds => CurrentTicks / TimeSpan.TicksPerMillisecond;
/// <summary>
/// Gets number of microseconds representing current UTC time.
/// </summary>
public static long CurrentMicroseconds => CurrentTicks * 1000 / TimeSpan.TicksPerMillisecond;
/// <summary>
/// Gets number of nanoseconds representing current UTC time.
/// </summary>
public static long CurrentNanoseconds => CurrentTicks * 1000 * 1000 / TimeSpan.TicksPerMillisecond;
/// <summary>
/// Get week of month according to the Gregorian calendar
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
public static int GetWeekOfMonth(this DateTime time)
{
var initialDayOfMonth = new DateTime(time.Year, time.Month, 1);
return time.GetGregorianWeekOfYear() - initialDayOfMonth.GetGregorianWeekOfYear() + 1;
}
private static int GetGregorianWeekOfYear(this DateTime time)
{
return GregorianCalendar.GetWeekOfYear(time, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
}
public static long GetTotalMinutes(this TimeSpan self)
{
return (long)self.TotalMinutes;
}
public static long GetTotalMilliseconds(this TimeSpan self)
{
return (long)self.TotalMilliseconds;
}
public static int GetMicroseconds(this DateTime self)
{
return (int)(self.Ticks / (TimeSpan.TicksPerMillisecond / 1000));
}
/// <summary>
/// Get all nanoseconds of last second, including milliseconds and microseconds.
/// </summary>
/// <param name="self"></param>
/// <returns>Return value could be be between 0 and 999999999</returns>
public static int GetNanosecondsOfSecond(this DateTime self)
{
return (int)(self.Ticks % TimeSpan.TicksPerSecond)
* NanosecondsPerTick;
}
/// <summary>
/// Get all nanoseconds of last second, including milliseconds and microseconds.
/// </summary>
/// <param name="self"></param>
/// <returns>Return value could be be between 0 and 999999999</returns>
public static int GetNanosecondsOfSecond(this DateTimeOffset self)
{
return (int)(self.Ticks % TimeSpan.TicksPerSecond)
* NanosecondsPerTick;
}
/// <summary>
/// Get nanoseconds of last millisecond, including microseconds.
/// </summary>
/// <param name="self"></param>
/// <returns>Return value could be be between 0 and 999999</returns>
public static int GetNanosecondsOfMillisecond(this DateTimeOffset self)
{
return (int)(self.Ticks % TimeSpan.TicksPerSecond % TimeSpan.TicksPerMillisecond)
* NanosecondsPerTick;
}
/// <summary>
/// Get nanoseconds of last millisecond, including microseconds.
/// </summary>
/// <returns>Return value could be be between 0 and 999999</returns>
public static DateTime GetDate(int year, int month, int weekOfMonth, DayOfWeek dayOfWeek)
{
var days = Enumerable.Range(1, DateTime.DaysInMonth(year, month))
.Select(x => new DateTime(year, month, x))
.Where(x => x.DayOfWeek == dayOfWeek)
.ToList();
return days.ElementAt(weekOfMonth - 1);
}
public static TimeSpan ParseZoneOffset(byte[] buffer, int offset, int count)
{
if (count == 0)
{
return LocalZoneOffset;
}
if (count == 1 && buffer[offset] == (byte)'Z')
{
return TimeSpan.Zero;
}
if (count == 3)
{
return DateTimeOffset.ParseExact(StringHelper.NewString(buffer, offset, 3), "zz",
CultureInfo.InvariantCulture).Offset;
}
if (count == 6)
{
return DateTimeOffset.ParseExact(StringHelper.NewString(buffer, offset, 6), "zzz",
CultureInfo.InvariantCulture).Offset;
}
throw new ArgumentException("Invalid time zone value");
}
public static string ToUniversalString(this DateTime self, TimestampPrecision precision)
{
var offset = self - self.ToUniversalTime();
return self.ToString(GetFormat(offset, precision));
}
public static string ToUniversalString(this DateTimeOffset self, TimestampPrecision precision)
{
return self.ToString(GetFormat(self.Offset, precision));
}
public static string ToTzUniversalString(this DateTime self, TimestampPrecision precision)
{
var offset = self - self.ToUniversalTime();
return self.ToString(GetFormat(offset, precision, true));
}
public static string ToTzUniversalString(this DateTimeOffset self, TimestampPrecision precision)
{
return self.ToString(GetFormat(self.Offset, precision, true));
}
private static string GetFormat(TimeSpan offset, TimestampPrecision precision = TimestampPrecision.Nano,
bool withTimeZone = false)
{
string format;
switch (precision)
{
case TimestampPrecision.Minute:
format = MinutesFormat;
break;
case TimestampPrecision.Milli:
format = MillisecondsFormat;
break;
case TimestampPrecision.Micro:
format = MicrosecondsFormat;
break;
case TimestampPrecision.Nano:
format = NanosecondsFormat;
break;
default:
format = GeneralFormat;
break;
}
if (!withTimeZone)
{
return format;
}
if (offset == TimeSpan.Zero)
{
return format + TimeZoneZeroSuffix;
}
if (offset.Minutes != 0)
{
return format + TimeZoneHoursAndMinutesSuffix;
}
return format + TimeZoneHoursSuffix;
}
/// <summary>
/// Converts amount of milliseconds <c>timestamp</c> to string representation using <c>format</c> string.
/// </summary>
/// <param name="timestamp">Milliseconds.</param>
/// <param name="format">Format string.</param>
/// <returns>Returns string value of DateTime converted from number of milliseconds.</returns>
public static string ToDateTimeString(this long timestamp, string format)
{
return FromMilliseconds(timestamp).ToString(format, CultureInfo.InvariantCulture);
}
/// <summary>
/// Gets total number of milliseconds from given DateTime value.
/// </summary>
/// <param name="self"></param>
/// <returns></returns>
public static long TotalMilliseconds(this DateTime self)
{
return self.Ticks / TimeSpan.TicksPerMillisecond;
}
/// <summary>
/// Gets total number of milliseconds from given DateTimeOffset value.
/// </summary>
/// <param name="self"></param>
/// <returns></returns>
public static long TotalMilliseconds(this DateTimeOffset self)
{
return self.Ticks / TimeSpan.TicksPerMillisecond;
}
/// <summary>
/// Creates DateTime <see cref="DateTime"/> from amount of milliseconds (UTC).
/// </summary>
/// <param name="milliseconds">Milliseconds passed fro</param>
/// <returns></returns>
public static DateTime FromMilliseconds(long milliseconds)
{
return new DateTime(milliseconds * TimeSpan.TicksPerMillisecond, DateTimeKind.Utc);
}
/// <summary>
/// Parse input <paramref name="timeZoneId">string</paramref> to <see cref="TimeSpan"/> offset from UTC.
/// Can use system time zone Id or try to parse strings like GMT+03:30.
/// </summary>
/// <param name="timeZoneId">Time zone Id.</param>
/// <param name="offset">Out parameter with parsed offset.</param>
/// <returns>Returns <see cref="TimeSpan"/> that represents time offset from UTC for given time zone Id.</returns>
public static bool TryParseTimeZoneOffset(string timeZoneId, out TimeSpan offset)
{
var isParsed = TryParseTimeZone(timeZoneId, out var result);
offset = result.BaseUtcOffset;
return isParsed;
}
internal static bool TryParseTimeZone(string timeZoneId, out TimeZoneInfo timeZoneInfo)
{
timeZoneInfo = TimeZoneInfo.Utc;
var logErrorMessage = $"Cannot parse time zone: {timeZoneId}";
try
{
timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
return true;
}
catch (TimeZoneNotFoundException)
{
if (!TryParseGmtPattern(timeZoneId, out var offset))
{
Log.Debug(logErrorMessage);
return false;
}
if (!IsUtcOffsetValid(offset))
{
Log.Debug("UTC offset must be within plus or minus 14.0 hours");
return false;
}
timeZoneInfo = CreateCustomTimeZone(offset);
return true;
}
catch (Exception)
{
Log.Debug(logErrorMessage);
}
return false;
}
private static TimeZoneInfo CreateCustomTimeZone(TimeSpan offset)
{
var customTimeZoneId = $"UTC{(offset < TimeSpan.Zero ? '-' : '+')}{offset:hh\\:mm}";
return TimeZoneInfo.CreateCustomTimeZone(customTimeZoneId, offset, "", "");
}
private static bool IsUtcOffsetValid(TimeSpan offset)
{
var maxOffset = TimeSpan.FromHours(14.0);
return offset >= -maxOffset && offset <= maxOffset;
}
private static bool TryParseGmtPattern(string pattern, out TimeSpan offset)
{
// trying to find and parse GMT pattern: GMT+05:30, GMT-3 or similar
var m = Regex.Match(pattern.Trim(), @"^(?:GMT|UTC) ?([+|-]\d{1,2}(:\d{2})?)?$");
if (m.Success)
{
// group 1 can be like "+2:30","-03" or empty
// empty Group 1 means UTC or GMT
if (m.Groups[1].Length == 0)
{
offset = UtcOffset;
return true;
}
// group 2 can be like ":30" or empty
var matched = m.Groups[2].Length == 0 ? m.Groups[1].Value + ":00" : m.Groups[1].Value;
if (TimeSpan.TryParse(matched.TrimStart('+'), out offset))
{
return true;
}
}
offset = UtcOffset;
return false;
}
}
}