R/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs
Mary 30ee70a9bc
time: Make TimeZoneRule blittable and avoid copies (#3361)
* time: Make TimeZoneRule blittable and avoid copies

This drastically reduce overhead of using TimeZoneRule around the
codebase.

Effect on games is unknown

* Add missing Box type

* Ensure we clean the structure still

This doesn't perform any copies

* Address gdkchan's comments

* Simplify Box
2022-06-24 19:04:57 +02:00

1702 lines
No EOL
53 KiB
C#

using Ryujinx.Common;
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.Utilities;
using System;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using static Ryujinx.HLE.HOS.Services.Time.TimeZone.TimeZoneRule;
namespace Ryujinx.HLE.HOS.Services.Time.TimeZone
{
public class TimeZone
{
private const int TimeTypeSize = 8;
private const int EpochYear = 1970;
private const int YearBase = 1900;
private const int EpochWeekDay = 4;
private const int SecondsPerMinute = 60;
private const int MinutesPerHour = 60;
private const int HoursPerDays = 24;
private const int DaysPerWekk = 7;
private const int DaysPerNYear = 365;
private const int DaysPerLYear = 366;
private const int MonthsPerYear = 12;
private const int SecondsPerHour = SecondsPerMinute * MinutesPerHour;
private const int SecondsPerDay = SecondsPerHour * HoursPerDays;
private const int YearsPerRepeat = 400;
private const long AverageSecondsPerYear = 31556952;
private const long SecondsPerRepeat = YearsPerRepeat * AverageSecondsPerYear;
private static readonly int[] YearLengths = { DaysPerNYear, DaysPerLYear };
private static readonly int[][] MonthsLengths = new int[][]
{
new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
new int[] { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
};
private static readonly byte[] TimeZoneDefaultRule = Encoding.ASCII.GetBytes(",M4.1.0,M10.5.0");
[StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x10)]
private struct CalendarTimeInternal
{
// NOTE: On the IPC side this is supposed to be a 16 bits value but internally this need to be a 64 bits value for ToPosixTime.
public long Year;
public sbyte Month;
public sbyte Day;
public sbyte Hour;
public sbyte Minute;
public sbyte Second;
public int CompareTo(CalendarTimeInternal other)
{
if (Year != other.Year)
{
if (Year < other.Year)
{
return -1;
}
return 1;
}
if (Month != other.Month)
{
return Month - other.Month;
}
if (Day != other.Day)
{
return Day - other.Day;
}
if (Hour != other.Hour)
{
return Hour - other.Hour;
}
if (Minute != other.Minute)
{
return Minute - other.Minute;
}
if (Second != other.Second)
{
return Second - other.Second;
}
return 0;
}
}
private enum RuleType
{
JulianDay,
DayOfYear,
MonthNthDayOfWeek
}
private struct Rule
{
public RuleType Type;
public int Day;
public int Week;
public int Month;
public int TransitionTime;
}
private static int Detzcode32(ReadOnlySpan<byte> bytes)
{
return BinaryPrimitives.ReadInt32BigEndian(bytes);
}
private static int Detzcode32(int value)
{
if (BitConverter.IsLittleEndian)
{
return BinaryPrimitives.ReverseEndianness(value);
}
return value;
}
private static long Detzcode64(ReadOnlySpan<byte> bytes)
{
return BinaryPrimitives.ReadInt64BigEndian(bytes);
}
private static bool DifferByRepeat(long t1, long t0)
{
return (t1 - t0) == SecondsPerRepeat;
}
private static bool TimeTypeEquals(in TimeZoneRule outRules, byte aIndex, byte bIndex)
{
if (aIndex < 0 || aIndex >= outRules.TypeCount || bIndex < 0 || bIndex >= outRules.TypeCount)
{
return false;
}
TimeTypeInfo a = outRules.Ttis[aIndex];
TimeTypeInfo b = outRules.Ttis[bIndex];
return a.GmtOffset == b.GmtOffset &&
a.IsDaySavingTime == b.IsDaySavingTime &&
a.IsStandardTimeDaylight == b.IsStandardTimeDaylight &&
a.IsGMT == b.IsGMT &&
StringUtils.CompareCStr(outRules.Chars[a.AbbreviationListIndex..], outRules.Chars[b.AbbreviationListIndex..]) == 0;
}
private static int GetQZName(ReadOnlySpan<byte> name, int namePosition, char delimiter)
{
int i = namePosition;
while (name[i] != '\0' && name[i] != delimiter)
{
i++;
}
return i;
}
private static int GetTZName(ReadOnlySpan<byte> name, int namePosition)
{
int i = namePosition;
char c;
while ((c = (char)name[i]) != '\0' && !char.IsDigit(c) && c != ',' && c != '-' && c != '+')
{
i++;
}
return i;
}
private static bool GetNum(ReadOnlySpan<byte> name, ref int namePosition, out int num, int min, int max)
{
num = 0;
if (namePosition >= name.Length)
{
return false;
}
char c = (char)name[namePosition];
if (!char.IsDigit(c))
{
return false;
}
do
{
num = num * 10 + (c - '0');
if (num > max)
{
return false;
}
if (++namePosition >= name.Length)
{
return false;
}
c = (char)name[namePosition];
}
while (char.IsDigit(c));
if (num < min)
{
return false;
}
return true;
}
private static bool GetSeconds(ReadOnlySpan<byte> name, ref int namePosition, out int seconds)
{
seconds = 0;
bool isValid = GetNum(name, ref namePosition, out int num, 0, HoursPerDays * DaysPerWekk - 1);
if (!isValid)
{
return false;
}
seconds = num * SecondsPerHour;
if (namePosition >= name.Length)
{
return false;
}
if (name[namePosition] == ':')
{
namePosition++;
isValid = GetNum(name, ref namePosition, out num, 0, MinutesPerHour - 1);
if (!isValid)
{
return false;
}
seconds += num * SecondsPerMinute;
if (namePosition >= name.Length)
{
return false;
}
if (name[namePosition] == ':')
{
namePosition++;
isValid = GetNum(name, ref namePosition, out num, 0, SecondsPerMinute);
if (!isValid)
{
return false;
}
seconds += num;
}
}
return true;
}
private static bool GetOffset(ReadOnlySpan<byte> name, ref int namePosition, ref int offset)
{
bool isNegative = false;
if (namePosition >= name.Length)
{
return false;
}
if (name[namePosition] == '-')
{
isNegative = true;
namePosition++;
}
else if (name[namePosition] == '+')
{
namePosition++;
}
if (namePosition >= name.Length)
{
return false;
}
bool isValid = GetSeconds(name, ref namePosition, out offset);
if (!isValid)
{
return false;
}
if (isNegative)
{
offset = -offset;
}
return true;
}
private static bool GetRule(ReadOnlySpan<byte> name, ref int namePosition, out Rule rule)
{
rule = new Rule();
bool isValid = false;
if (name[namePosition] == 'J')
{
namePosition++;
rule.Type = RuleType.JulianDay;
isValid = GetNum(name, ref namePosition, out rule.Day, 1, DaysPerNYear);
}
else if (name[namePosition] == 'M')
{
namePosition++;
rule.Type = RuleType.MonthNthDayOfWeek;
isValid = GetNum(name, ref namePosition, out rule.Month, 1, MonthsPerYear);
if (!isValid)
{
return false;
}
if (name[namePosition++] != '.')
{
return false;
}
isValid = GetNum(name, ref namePosition, out rule.Week, 1, 5);
if (!isValid)
{
return false;
}
if (name[namePosition++] != '.')
{
return false;
}
isValid = GetNum(name, ref namePosition, out rule.Day, 0, DaysPerWekk - 1);
}
else if (char.IsDigit((char)name[namePosition]))
{
rule.Type = RuleType.DayOfYear;
isValid = GetNum(name, ref namePosition, out rule.Day, 0, DaysPerLYear - 1);
}
else
{
return false;
}
if (!isValid)
{
return false;
}
if (name[namePosition] == '/')
{
namePosition++;
return GetOffset(name, ref namePosition, ref rule.TransitionTime);
}
else
{
rule.TransitionTime = 2 * SecondsPerHour;
}
return true;
}
private static int IsLeap(int year)
{
if (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0))
{
return 1;
}
return 0;
}
private static bool ParsePosixName(ReadOnlySpan<byte> name, ref TimeZoneRule outRules, bool lastDitch)
{
outRules = new TimeZoneRule();
int stdLen;
ReadOnlySpan<byte> stdName = name;
int namePosition = 0;
int stdOffset = 0;
if (lastDitch)
{
stdLen = 3;
namePosition += stdLen;
}
else
{
if (name[namePosition] == '<')
{
namePosition++;
stdName = name.Slice(namePosition);
int stdNamePosition = namePosition;
namePosition = GetQZName(name, namePosition, '>');
if (name[namePosition] != '>')
{
return false;
}
stdLen = namePosition - stdNamePosition;
namePosition++;
}
else
{
namePosition = GetTZName(name, namePosition);
stdLen = namePosition;
}
if (stdLen == 0)
{
return false;
}
bool isValid = GetOffset(name.ToArray(), ref namePosition, ref stdOffset);
if (!isValid)
{
return false;
}
}
int charCount = stdLen + 1;
int destLen = 0;
int dstOffset = 0;
ReadOnlySpan<byte> destName = name.Slice(namePosition);
if (TzCharsArraySize < charCount)
{
return false;
}
if (name[namePosition] != '\0')
{
if (name[namePosition] == '<')
{
destName = name.Slice(++namePosition);
int destNamePosition = namePosition;
namePosition = GetQZName(name.ToArray(), namePosition, '>');
if (name[namePosition] != '>')
{
return false;
}
destLen = namePosition - destNamePosition;
namePosition++;
}
else
{
destName = name.Slice(namePosition);
namePosition = GetTZName(name, namePosition);
destLen = namePosition;
}
if (destLen == 0)
{
return false;
}
charCount += destLen + 1;
if (TzCharsArraySize < charCount)
{
return false;
}
if (name[namePosition] != '\0' && name[namePosition] != ',' && name[namePosition] != ';')
{
bool isValid = GetOffset(name.ToArray(), ref namePosition, ref dstOffset);
if (!isValid)
{
return false;
}
}
else
{
dstOffset = stdOffset - SecondsPerHour;
}
if (name[namePosition] == '\0')
{
name = TimeZoneDefaultRule;
namePosition = 0;
}
if (name[namePosition] == ',' || name[namePosition] == ';')
{
namePosition++;
bool IsRuleValid = GetRule(name, ref namePosition, out Rule start);
if (!IsRuleValid)
{
return false;
}
if (name[namePosition++] != ',')
{
return false;
}
IsRuleValid = GetRule(name, ref namePosition, out Rule end);
if (!IsRuleValid)
{
return false;
}
if (name[namePosition] != '\0')
{
return false;
}
outRules.TypeCount = 2;
outRules.Ttis[0] = new TimeTypeInfo
{
GmtOffset = -dstOffset,
IsDaySavingTime = true,
AbbreviationListIndex = stdLen + 1
};
outRules.Ttis[1] = new TimeTypeInfo
{
GmtOffset = -stdOffset,
IsDaySavingTime = false,
AbbreviationListIndex = 0
};
outRules.DefaultType = 0;
int timeCount = 0;
long janFirst = 0;
int janOffset = 0;
int yearBegining = EpochYear;
do
{
int yearSeconds = YearLengths[IsLeap(yearBegining - 1)] * SecondsPerDay;
yearBegining--;
if (IncrementOverflow64(ref janFirst, -yearSeconds))
{
janOffset = -yearSeconds;
break;
}
}
while (EpochYear - YearsPerRepeat / 2 < yearBegining);
int yearLimit = yearBegining + YearsPerRepeat + 1;
int year;
for (year = yearBegining; year < yearLimit; year++)
{
int startTime = TransitionTime(year, start, stdOffset);
int endTime = TransitionTime(year, end, dstOffset);
int yearSeconds = YearLengths[IsLeap(year)] * SecondsPerDay;
bool isReversed = endTime < startTime;
if (isReversed)
{
int swap = startTime;
startTime = endTime;
endTime = swap;
}
if (isReversed || (startTime < endTime && (endTime - startTime < (yearSeconds + (stdOffset - dstOffset)))))
{
if (TzMaxTimes - 2 < timeCount)
{
break;
}
outRules.Ats[timeCount] = janFirst;
if (!IncrementOverflow64(ref outRules.Ats[timeCount], janOffset + startTime))
{
outRules.Types[timeCount++] = isReversed ? (byte)1 : (byte)0;
}
else if (janOffset != 0)
{
outRules.DefaultType = isReversed ? 1 : 0;
}
outRules.Ats[timeCount] = janFirst;
if (!IncrementOverflow64(ref outRules.Ats[timeCount], janOffset + endTime))
{
outRules.Types[timeCount++] = isReversed ? (byte)0 : (byte)1;
yearLimit = year + YearsPerRepeat + 1;
}
else if (janOffset != 0)
{
outRules.DefaultType = isReversed ? 0 : 1;
}
}
if (IncrementOverflow64(ref janFirst, janOffset + yearSeconds))
{
break;
}
janOffset = 0;
}
outRules.TimeCount = timeCount;
// There is no time variation, this is then a perpetual DST rule
if (timeCount == 0)
{
outRules.TypeCount = 1;
}
else if (YearsPerRepeat < year - yearBegining)
{
outRules.GoBack = true;
outRules.GoAhead = true;
}
}
else
{
if (name[namePosition] == '\0')
{
return false;
}
long theirStdOffset = 0;
for (int i = 0; i < outRules.TimeCount; i++)
{
int j = outRules.Types[i];
if (outRules.Ttis[j].IsStandardTimeDaylight)
{
theirStdOffset = -outRules.Ttis[j].GmtOffset;
}
}
long theirDstOffset = 0;
for (int i = 0; i < outRules.TimeCount; i++)
{
int j = outRules.Types[i];
if (outRules.Ttis[j].IsDaySavingTime)
{
theirDstOffset = -outRules.Ttis[j].GmtOffset;
}
}
bool isDaySavingTime = false;
long theirOffset = theirStdOffset;
for (int i = 0; i < outRules.TimeCount; i++)
{
int j = outRules.Types[i];
outRules.Types[i] = outRules.Ttis[j].IsDaySavingTime ? (byte)1 : (byte)0;
if (!outRules.Ttis[j].IsGMT)
{
if (isDaySavingTime && !outRules.Ttis[j].IsStandardTimeDaylight)
{
outRules.Ats[i] += dstOffset - theirStdOffset;
}
else
{
outRules.Ats[i] += stdOffset - theirStdOffset;
}
}
theirOffset = -outRules.Ttis[j].GmtOffset;
if (outRules.Ttis[j].IsDaySavingTime)
{
theirDstOffset = theirOffset;
}
else
{
theirStdOffset = theirOffset;
}
}
outRules.Ttis[0] = new TimeTypeInfo
{
GmtOffset = -stdOffset,
IsDaySavingTime = false,
AbbreviationListIndex = 0
};
outRules.Ttis[1] = new TimeTypeInfo
{
GmtOffset = -dstOffset,
IsDaySavingTime = true,
AbbreviationListIndex = stdLen + 1
};
outRules.TypeCount = 2;
outRules.DefaultType = 0;
}
}
else
{
// default is perpetual standard time
outRules.TypeCount = 1;
outRules.TimeCount = 0;
outRules.DefaultType = 0;
outRules.Ttis[0] = new TimeTypeInfo
{
GmtOffset = -stdOffset,
IsDaySavingTime = false,
AbbreviationListIndex = 0
};
}
outRules.CharCount = charCount;
int charsPosition = 0;
for (int i = 0; i < stdLen; i++)
{
outRules.Chars[i] = stdName[i];
}
charsPosition += stdLen;
outRules.Chars[charsPosition++] = 0;
if (destLen != 0)
{
for (int i = 0; i < destLen; i++)
{
outRules.Chars[charsPosition + i] = destName[i];
}
outRules.Chars[charsPosition + destLen] = 0;
}
return true;
}
private static int TransitionTime(int year, Rule rule, int offset)
{
int leapYear = IsLeap(year);
int value;
switch (rule.Type)
{
case RuleType.JulianDay:
value = (rule.Day - 1) * SecondsPerDay;
if (leapYear == 1 && rule.Day >= 60)
{
value += SecondsPerDay;
}
break;
case RuleType.DayOfYear:
value = rule.Day * SecondsPerDay;
break;
case RuleType.MonthNthDayOfWeek:
// Here we use Zeller's Congruence to get the day of week of the first month.
int m1 = (rule.Month + 9) % 12 + 1;
int yy0 = (rule.Month <= 2) ? (year - 1) : year;
int yy1 = yy0 / 100;
int yy2 = yy0 % 100;
int dayOfWeek = ((26 * m1 - 2) / 10 + 1 + yy2 + yy2 / 4 + yy1 / 4 - 2 * yy1) % 7;
if (dayOfWeek < 0)
{
dayOfWeek += DaysPerWekk;
}
// Get the zero origin
int d = rule.Day - dayOfWeek;
if (d < 0)
{
d += DaysPerWekk;
}
for (int i = 1; i < rule.Week; i++)
{
if (d + DaysPerWekk >= MonthsLengths[leapYear][rule.Month - 1])
{
break;
}
d += DaysPerWekk;
}
value = d * SecondsPerDay;
for (int i = 0; i < rule.Month - 1; i++)
{
value += MonthsLengths[leapYear][i] * SecondsPerDay;
}
break;
default:
throw new NotImplementedException("Unknown time transition!");
}
return value + rule.TransitionTime + offset;
}
private static bool NormalizeOverflow32(ref int ip, ref int unit, int baseValue)
{
int delta;
if (unit >= 0)
{
delta = unit / baseValue;
}
else
{
delta = -1 - (-1 - unit) / baseValue;
}
unit -= delta * baseValue;
return IncrementOverflow32(ref ip, delta);
}
private static bool NormalizeOverflow64(ref long ip, ref long unit, long baseValue)
{
long delta;
if (unit >= 0)
{
delta = unit / baseValue;
}
else
{
delta = -1 - (-1 - unit) / baseValue;
}
unit -= delta * baseValue;
return IncrementOverflow64(ref ip, delta);
}
private static bool IncrementOverflow32(ref int time, int j)
{
try
{
time = checked(time + j);
return false;
}
catch (OverflowException)
{
return true;
}
}
private static bool IncrementOverflow64(ref long time, long j)
{
try
{
time = checked(time + j);
return false;
}
catch (OverflowException)
{
return true;
}
}
internal static bool ParsePosixName(string name, ref TimeZoneRule outRules)
{
return ParsePosixName(Encoding.ASCII.GetBytes(name), ref outRules, false);
}
internal static bool ParseTimeZoneBinary(ref TimeZoneRule outRules, Stream inputData)
{
outRules = new TimeZoneRule();
BinaryReader reader = new BinaryReader(inputData);
long streamLength = reader.BaseStream.Length;
if (streamLength < Marshal.SizeOf<TzifHeader>())
{
return false;
}
TzifHeader header = reader.ReadStruct<TzifHeader>();
streamLength -= Marshal.SizeOf<TzifHeader>();
int ttisGMTCount = Detzcode32(header.TtisGMTCount);
int ttisSTDCount = Detzcode32(header.TtisSTDCount);
int leapCount = Detzcode32(header.LeapCount);
int timeCount = Detzcode32(header.TimeCount);
int typeCount = Detzcode32(header.TypeCount);
int charCount = Detzcode32(header.CharCount);
if (!(0 <= leapCount
&& leapCount < TzMaxLeaps
&& 0 < typeCount
&& typeCount < TzMaxTypes
&& 0 <= timeCount
&& timeCount < TzMaxTimes
&& 0 <= charCount
&& charCount < TzMaxChars
&& (ttisSTDCount == typeCount || ttisSTDCount == 0)
&& (ttisGMTCount == typeCount || ttisGMTCount == 0)))
{
return false;
}
if (streamLength < (timeCount * TimeTypeSize
+ timeCount
+ typeCount * 6
+ charCount
+ leapCount * (TimeTypeSize + 4)
+ ttisSTDCount
+ ttisGMTCount))
{
return false;
}
outRules.TimeCount = timeCount;
outRules.TypeCount = typeCount;
outRules.CharCount = charCount;
byte[] workBuffer = StreamUtils.StreamToBytes(inputData);
timeCount = 0;
{
Span<byte> p = workBuffer;
for (int i = 0; i < outRules.TimeCount; i++)
{
long at = Detzcode64(p);
outRules.Types[i] = 1;
if (timeCount != 0 && at <= outRules.Ats[timeCount - 1])
{
if (at < outRules.Ats[timeCount - 1])
{
return false;
}
outRules.Types[i - 1] = 0;
timeCount--;
}
outRules.Ats[timeCount++] = at;
p = p[TimeTypeSize..];
}
timeCount = 0;
for (int i = 0; i < outRules.TimeCount; i++)
{
byte type = p[0];
p = p[1..];
if (outRules.TypeCount <= type)
{
return false;
}
if (outRules.Types[i] != 0)
{
outRules.Types[timeCount++] = type;
}
}
outRules.TimeCount = timeCount;
for (int i = 0; i < outRules.TypeCount; i++)
{
TimeTypeInfo ttis = outRules.Ttis[i];
ttis.GmtOffset = Detzcode32(p);
p = p[sizeof(int)..];
if (p[0] >= 2)
{
return false;
}
ttis.IsDaySavingTime = p[0] != 0;
p = p[1..];
int abbreviationListIndex = p[0];
p = p[1..];
if (abbreviationListIndex >= outRules.CharCount)
{
return false;
}
ttis.AbbreviationListIndex = abbreviationListIndex;
outRules.Ttis[i] = ttis;
}
p[..outRules.CharCount].CopyTo(outRules.Chars);
p = p[outRules.CharCount..];
outRules.Chars[outRules.CharCount] = 0;
for (int i = 0; i < outRules.TypeCount; i++)
{
if (ttisSTDCount == 0)
{
outRules.Ttis[i].IsStandardTimeDaylight = false;
}
else
{
if (p[0] >= 2)
{
return false;
}
outRules.Ttis[i].IsStandardTimeDaylight = p[0] != 0;
p = p[1..];
}
}
for (int i = 0; i < outRules.TypeCount; i++)
{
if (ttisSTDCount == 0)
{
outRules.Ttis[i].IsGMT = false;
}
else
{
if (p[0] >= 2)
{
return false;
}
outRules.Ttis[i].IsGMT = p[0] != 0;
p = p[1..];
}
}
long position = (workBuffer.Length - p.Length);
long nRead = streamLength - position;
if (nRead < 0)
{
return false;
}
// Nintendo abort in case of a TzIf file with a POSIX TZ Name too long to fit inside a TimeZoneRule.
// As it's impossible in normal usage to achive this, we also force a crash.
if (nRead > (TzNameMax + 1))
{
throw new InvalidOperationException();
}
byte[] tempName = new byte[TzNameMax + 1];
Array.Copy(workBuffer, position, tempName, 0, nRead);
if (nRead > 2 && tempName[0] == '\n' && tempName[nRead - 1] == '\n' && outRules.TypeCount + 2 <= TzMaxTypes)
{
tempName[nRead - 1] = 0;
byte[] name = new byte[TzNameMax];
Array.Copy(tempName, 1, name, 0, nRead - 1);
Box<TimeZoneRule> tempRulesBox = new Box<TimeZoneRule>();
ref TimeZoneRule tempRules = ref tempRulesBox.Data;
if (ParsePosixName(name, ref tempRulesBox.Data, false))
{
int abbreviationCount = 0;
charCount = outRules.CharCount;
Span<byte> chars = outRules.Chars;
for (int i = 0; i < tempRules.TypeCount; i++)
{
ReadOnlySpan<byte> tempChars = tempRules.Chars;
ReadOnlySpan<byte> tempAbbreviation = tempChars[tempRules.Ttis[i].AbbreviationListIndex..];
int j;
for (j = 0; j < charCount; j++)
{
if (StringUtils.CompareCStr(chars[j..], tempAbbreviation) == 0)
{
tempRules.Ttis[i].AbbreviationListIndex = j;
abbreviationCount++;
break;
}
}
if (j >= charCount)
{
int abbreviationLength = StringUtils.LengthCstr(tempAbbreviation);
if (j + abbreviationLength < TzMaxChars)
{
for (int x = 0; x < abbreviationLength; x++)
{
chars[j + x] = tempAbbreviation[x];
}
charCount = j + abbreviationLength + 1;
tempRules.Ttis[i].AbbreviationListIndex = j;
abbreviationCount++;
}
}
}
if (abbreviationCount == tempRules.TypeCount)
{
outRules.CharCount = charCount;
// Remove trailing
while (1 < outRules.TimeCount && (outRules.Types[outRules.TimeCount - 1] == outRules.Types[outRules.TimeCount - 2]))
{
outRules.TimeCount--;
}
int i;
for (i = 0; i < tempRules.TimeCount; i++)
{
if (outRules.TimeCount == 0 || outRules.Ats[outRules.TimeCount - 1] < tempRules.Ats[i])
{
break;
}
}
while (i < tempRules.TimeCount && outRules.TimeCount < TzMaxTimes)
{
outRules.Ats[outRules.TimeCount] = tempRules.Ats[i];
outRules.Types[outRules.TimeCount] = (byte)(outRules.TypeCount + (byte)tempRules.Types[i]);
outRules.TimeCount++;
i++;
}
for (i = 0; i < tempRules.TypeCount; i++)
{
outRules.Ttis[outRules.TypeCount++] = tempRules.Ttis[i];
}
}
}
}
if (outRules.TypeCount == 0)
{
return false;
}
if (outRules.TimeCount > 1)
{
for (int i = 1; i < outRules.TimeCount; i++)
{
if (TimeTypeEquals(in outRules, outRules.Types[i], outRules.Types[0]) && DifferByRepeat(outRules.Ats[i], outRules.Ats[0]))
{
outRules.GoBack = true;
break;
}
}
for (int i = outRules.TimeCount - 2; i >= 0; i--)
{
if (TimeTypeEquals(in outRules, outRules.Types[outRules.TimeCount - 1], outRules.Types[i]) && DifferByRepeat(outRules.Ats[outRules.TimeCount - 1], outRules.Ats[i]))
{
outRules.GoAhead = true;
break;
}
}
}
int defaultType;
for (defaultType = 0; defaultType < outRules.TimeCount; defaultType++)
{
if (outRules.Types[defaultType] == 0)
{
break;
}
}
defaultType = defaultType < outRules.TimeCount ? -1 : 0;
if (defaultType < 0 && outRules.TimeCount > 0 && outRules.Ttis[outRules.Types[0]].IsDaySavingTime)
{
defaultType = outRules.Types[0];
while (--defaultType >= 0)
{
if (!outRules.Ttis[defaultType].IsDaySavingTime)
{
break;
}
}
}
if (defaultType < 0)
{
defaultType = 0;
while (outRules.Ttis[defaultType].IsDaySavingTime)
{
if (++defaultType >= outRules.TypeCount)
{
defaultType = 0;
break;
}
}
}
outRules.DefaultType = defaultType;
}
return true;
}
private static long GetLeapDaysNotNeg(long year)
{
return year / 4 - year / 100 + year / 400;
}
private static long GetLeapDays(long year)
{
if (year < 0)
{
return -1 - GetLeapDaysNotNeg(-1 - year);
}
else
{
return GetLeapDaysNotNeg(year);
}
}
private static ResultCode CreateCalendarTime(long time, int gmtOffset, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo)
{
long year = EpochYear;
long timeDays = time / SecondsPerDay;
long remainingSeconds = time % SecondsPerDay;
calendarTime = new CalendarTimeInternal();
calendarAdditionalInfo = new CalendarAdditionalInfo();
while (timeDays < 0 || timeDays >= YearLengths[IsLeap((int)year)])
{
long timeDelta = timeDays / DaysPerLYear;
long delta = timeDelta;
if (delta == 0)
{
delta = timeDays < 0 ? -1 : 1;
}
long newYear = year;
if (IncrementOverflow64(ref newYear, delta))
{
return ResultCode.OutOfRange;
}
long leapDays = GetLeapDays(newYear - 1) - GetLeapDays(year - 1);
timeDays -= (newYear - year) * DaysPerNYear;
timeDays -= leapDays;
year = newYear;
}
long dayOfYear = timeDays;
remainingSeconds += gmtOffset;
while (remainingSeconds < 0)
{
remainingSeconds += SecondsPerDay;
dayOfYear -= 1;
}
while (remainingSeconds >= SecondsPerDay)
{
remainingSeconds -= SecondsPerDay;
dayOfYear += 1;
}
while (dayOfYear < 0)
{
if (IncrementOverflow64(ref year, -1))
{
return ResultCode.OutOfRange;
}
dayOfYear += YearLengths[IsLeap((int)year)];
}
while (dayOfYear >= YearLengths[IsLeap((int)year)])
{
dayOfYear -= YearLengths[IsLeap((int)year)];
if (IncrementOverflow64(ref year, 1))
{
return ResultCode.OutOfRange;
}
}
calendarTime.Year = year;
calendarAdditionalInfo.DayOfYear = (uint)dayOfYear;
long dayOfWeek = (EpochWeekDay + ((year - EpochYear) % DaysPerWekk) * (DaysPerNYear % DaysPerWekk) + GetLeapDays(year - 1) - GetLeapDays(EpochYear - 1) + dayOfYear) % DaysPerWekk;
if (dayOfWeek < 0)
{
dayOfWeek += DaysPerWekk;
}
calendarAdditionalInfo.DayOfWeek = (uint)dayOfWeek;
calendarTime.Hour = (sbyte)((remainingSeconds / SecondsPerHour) % SecondsPerHour);
remainingSeconds %= SecondsPerHour;
calendarTime.Minute = (sbyte)(remainingSeconds / SecondsPerMinute);
calendarTime.Second = (sbyte)(remainingSeconds % SecondsPerMinute);
int[] ip = MonthsLengths[IsLeap((int)year)];
for (calendarTime.Month = 0; dayOfYear >= ip[calendarTime.Month]; ++calendarTime.Month)
{
dayOfYear -= ip[calendarTime.Month];
}
calendarTime.Day = (sbyte)(dayOfYear + 1);
calendarAdditionalInfo.IsDaySavingTime = false;
calendarAdditionalInfo.GmtOffset = gmtOffset;
return 0;
}
private static ResultCode ToCalendarTimeInternal(in TimeZoneRule rules, long time, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo)
{
calendarTime = new CalendarTimeInternal();
calendarAdditionalInfo = new CalendarAdditionalInfo();
ResultCode result;
if ((rules.GoAhead && time < rules.Ats[0]) || (rules.GoBack && time > rules.Ats[rules.TimeCount - 1]))
{
long newTime = time;
long seconds;
long years;
if (time < rules.Ats[0])
{
seconds = rules.Ats[0] - time;
}
else
{
seconds = time - rules.Ats[rules.TimeCount - 1];
}
seconds -= 1;
years = (seconds / SecondsPerRepeat + 1) * YearsPerRepeat;
seconds = years * AverageSecondsPerYear;
if (time < rules.Ats[0])
{
newTime += seconds;
}
else
{
newTime -= seconds;
}
if (newTime < rules.Ats[0] && newTime > rules.Ats[rules.TimeCount - 1])
{
return ResultCode.TimeNotFound;
}
result = ToCalendarTimeInternal(in rules, newTime, out calendarTime, out calendarAdditionalInfo);
if (result != 0)
{
return result;
}
if (time < rules.Ats[0])
{
calendarTime.Year -= years;
}
else
{
calendarTime.Year += years;
}
return ResultCode.Success;
}
int ttiIndex;
if (rules.TimeCount == 0 || time < rules.Ats[0])
{
ttiIndex = rules.DefaultType;
}
else
{
int low = 1;
int high = rules.TimeCount;
while (low < high)
{
int mid = (low + high) >> 1;
if (time < rules.Ats[mid])
{
high = mid;
}
else
{
low = mid + 1;
}
}
ttiIndex = rules.Types[low - 1];
}
result = CreateCalendarTime(time, rules.Ttis[ttiIndex].GmtOffset, out calendarTime, out calendarAdditionalInfo);
if (result == 0)
{
calendarAdditionalInfo.IsDaySavingTime = rules.Ttis[ttiIndex].IsDaySavingTime;
ReadOnlySpan<byte> timeZoneAbbreviation = rules.Chars[rules.Ttis[ttiIndex].AbbreviationListIndex..];
int timeZoneSize = Math.Min(StringUtils.LengthCstr(timeZoneAbbreviation), 8);
timeZoneAbbreviation[..timeZoneSize].CopyTo(calendarAdditionalInfo.TimezoneName.ToSpan());
}
return result;
}
private static ResultCode ToPosixTimeInternal(in TimeZoneRule rules, CalendarTimeInternal calendarTime, out long posixTime)
{
posixTime = 0;
int hour = calendarTime.Hour;
int minute = calendarTime.Minute;
if (NormalizeOverflow32(ref hour, ref minute, MinutesPerHour))
{
return ResultCode.Overflow;
}
calendarTime.Minute = (sbyte)minute;
int day = calendarTime.Day;
if (NormalizeOverflow32(ref day, ref hour, HoursPerDays))
{
return ResultCode.Overflow;
}
calendarTime.Day = (sbyte)day;
calendarTime.Hour = (sbyte)hour;
long year = calendarTime.Year;
long month = calendarTime.Month;
if (NormalizeOverflow64(ref year, ref month, MonthsPerYear))
{
return ResultCode.Overflow;
}
calendarTime.Month = (sbyte)month;
if (IncrementOverflow64(ref year, YearBase))
{
return ResultCode.Overflow;
}
while (day <= 0)
{
if (IncrementOverflow64(ref year, -1))
{
return ResultCode.Overflow;
}
long li = year;
if (1 < calendarTime.Month)
{
li++;
}
day += YearLengths[IsLeap((int)li)];
}
while (day > DaysPerLYear)
{
long li = year;
if (1 < calendarTime.Month)
{
li++;
}
day -= YearLengths[IsLeap((int)li)];
if (IncrementOverflow64(ref year, 1))
{
return ResultCode.Overflow;
}
}
while (true)
{
int i = MonthsLengths[IsLeap((int)year)][calendarTime.Month];
if (day <= i)
{
break;
}
day -= i;
calendarTime.Month += 1;
if (calendarTime.Month >= MonthsPerYear)
{
calendarTime.Month = 0;
if (IncrementOverflow64(ref year, 1))
{
return ResultCode.Overflow;
}
}
}
calendarTime.Day = (sbyte)day;
if (IncrementOverflow64(ref year, -YearBase))
{
return ResultCode.Overflow;
}
calendarTime.Year = year;
int savedSeconds;
if (calendarTime.Second >= 0 && calendarTime.Second < SecondsPerMinute)
{
savedSeconds = 0;
}
else if (year + YearBase < EpochYear)
{
int second = calendarTime.Second;
if (IncrementOverflow32(ref second, 1 - SecondsPerMinute))
{
return ResultCode.Overflow;
}
savedSeconds = second;
calendarTime.Second = 1 - SecondsPerMinute;
}
else
{
savedSeconds = calendarTime.Second;
calendarTime.Second = 0;
}
long low = long.MinValue;
long high = long.MaxValue;
while (true)
{
long pivot = low / 2 + high / 2;
if (pivot < low)
{
pivot = low;
}
else if (pivot > high)
{
pivot = high;
}
int direction;
ResultCode result = ToCalendarTimeInternal(in rules, pivot, out CalendarTimeInternal candidateCalendarTime, out _);
if (result != 0)
{
if (pivot > 0)
{
direction = 1;
}
else
{
direction = -1;
}
}
else
{
direction = candidateCalendarTime.CompareTo(calendarTime);
}
if (direction == 0)
{
long timeResult = pivot + savedSeconds;
if ((timeResult < pivot) != (savedSeconds < 0))
{
return ResultCode.Overflow;
}
posixTime = timeResult;
break;
}
else
{
if (pivot == low)
{
if (pivot == long.MaxValue)
{
return ResultCode.TimeNotFound;
}
pivot += 1;
low += 1;
}
else if (pivot == high)
{
if (pivot == long.MinValue)
{
return ResultCode.TimeNotFound;
}
pivot -= 1;
high -= 1;
}
if (low > high)
{
return ResultCode.TimeNotFound;
}
if (direction > 0)
{
high = pivot;
}
else
{
low = pivot;
}
}
}
return ResultCode.Success;
}
internal static ResultCode ToCalendarTime(in TimeZoneRule rules, long time, out CalendarInfo calendar)
{
ResultCode result = ToCalendarTimeInternal(in rules, time, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo);
calendar = new CalendarInfo()
{
Time = new CalendarTime()
{
Year = (short)calendarTime.Year,
// NOTE: Nintendo's month range is 1-12, internal range is 0-11.
Month = (sbyte)(calendarTime.Month + 1),
Day = calendarTime.Day,
Hour = calendarTime.Hour,
Minute = calendarTime.Minute,
Second = calendarTime.Second
},
AdditionalInfo = calendarAdditionalInfo
};
return result;
}
internal static ResultCode ToPosixTime(in TimeZoneRule rules, CalendarTime calendarTime, out long posixTime)
{
CalendarTimeInternal calendarTimeInternal = new CalendarTimeInternal()
{
Year = calendarTime.Year,
// NOTE: Nintendo's month range is 1-12, internal range is 0-11.
Month = (sbyte)(calendarTime.Month - 1),
Day = calendarTime.Day,
Hour = calendarTime.Hour,
Minute = calendarTime.Minute,
Second = calendarTime.Second
};
return ToPosixTimeInternal(in rules, calendarTimeInternal, out posixTime);
}
}
}