Skip to content

Commit b9a1654

Browse files
committed
PostgreSqlHelper: Change getting timezone
1 parent 540a9c5 commit b9a1654

File tree

2 files changed

+188
-57
lines changed

2 files changed

+188
-57
lines changed

Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/PostgreSqlHelper.cs

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
// See the License.txt file in the project root for more information.
44

55
using System;
6-
using System.Collections.Generic;
7-
using Npgsql;
6+
using System.Globalization;
7+
using System.Text.RegularExpressions;
88
using NpgsqlTypes;
99
using Xtensive.Orm.PostgreSql;
1010

@@ -52,20 +52,81 @@ internal static TimeSpan ResurrectTimeSpanFromNpgsqlInterval(in NpgsqlInterval n
5252
}
5353

5454
/// <summary>
55-
/// Checks if timezone is declared in POSIX format (example &lt;+07&gt;-07 )
56-
/// and returns number between '&lt;' and '&gt;' as timezone.
55+
/// Gets system time zone info for server time zone, if such zone exists.
5756
/// </summary>
58-
/// <param name="timezone">Timezone in possible POSIX format</param>
59-
/// <returns>Timezone shift declared in oritinal POSIX format as timezone or original value.</returns>
60-
internal static string TryGetZoneFromPosix(string timezone)
57+
/// <param name="connectionTimezone">Time zone from connection</param>
58+
/// <returns>Instance of <see cref="TimeZoneInfo"/> if such found, or <see langword="null"/>.</returns>
59+
/// <exception cref="ArgumentException">Server timezone offset can't be recognized.</exception>
60+
public static TimeZoneInfo GetTimeZoneInfoForServerTimeZone(string connectionTimezone)
6161
{
62-
if (timezone.StartsWith('<')) {
63-
// if POSIX format
64-
var closing = timezone.IndexOf('>');
65-
var result = timezone.Substring(1, closing - 1);
62+
if (string.IsNullOrEmpty(connectionTimezone)) {
63+
return null;
64+
}
65+
66+
// Try to get zone as is, conversion from IANA format identifier
67+
// happens inside the TimeZoneInfo.FindSystemTimeZoneById().
68+
// Postgres uses IANA timezone format identifier, not windows one.
69+
if (TryFindSystemTimeZoneById(connectionTimezone, out var result)) {
6670
return result;
6771
}
68-
return timezone;
72+
73+
// If zone was set as certain offset, then it will be returned to us in form of
74+
// POSIX offset, e.g. '<+03>-03' for UTC+03 or '<+1030>-1030' for UTC+10:30
75+
if (Regex.IsMatch(connectionTimezone, "^<[+|-]\\d{2,4}>[-|+]\\d{2,4}$")) {
76+
var closingBracketIndex = connectionTimezone.IndexOf('>');
77+
var utcOffset = connectionTimezone.Substring(1, closingBracketIndex - 1);
78+
79+
var utcOffsetString = utcOffset.Length switch {
80+
3 => utcOffset,
81+
5 => utcOffset.Insert(3, ":"),
82+
_ => string.Empty
83+
};
84+
85+
//Here, we rely on server validation of zone existance for the offset required by user
86+
87+
var utcIdentifier = $"UTC{utcOffsetString}";
88+
89+
if (utcIdentifier.Length == 3)
90+
throw new ArgumentException($"Server connection time zone '{connectionTimezone}' cannot be recongized.");
91+
92+
if (TryFindSystemTimeZoneById(utcIdentifier, out var utcTimeZone)) {
93+
return utcTimeZone;
94+
}
95+
else {
96+
var parsingCulture = CultureInfo.InvariantCulture;
97+
TimeSpan baseOffset;
98+
if (utcOffsetString.StartsWith("-")) {
99+
if (!TimeSpan.TryParseExact(utcOffsetString, "\\-hh\\:mm", parsingCulture, TimeSpanStyles.AssumeNegative, out baseOffset))
100+
if(!TimeSpan.TryParseExact(utcOffsetString, "\\-hh", parsingCulture, TimeSpanStyles.AssumeNegative, out baseOffset))
101+
throw new ArgumentException($"Server connection time zone '{connectionTimezone}' cannot be recongized.");
102+
}
103+
else {
104+
if (!TimeSpan.TryParseExact(utcOffsetString, "\\+hh\\:mm", parsingCulture, TimeSpanStyles.None, out baseOffset))
105+
if (!TimeSpan.TryParseExact(utcOffsetString, "\\+hh", parsingCulture, TimeSpanStyles.None, out baseOffset))
106+
throw new ArgumentException($"Server connection time zone '{connectionTimezone}' cannot be recongized.");
107+
}
108+
109+
return TimeZoneInfo.CreateCustomTimeZone(utcIdentifier, baseOffset, "Coordinated Universal Time" + utcOffsetString, utcIdentifier);
110+
}
111+
}
112+
113+
return null;
114+
}
115+
116+
private static bool TryFindSystemTimeZoneById(string id, out TimeZoneInfo timeZoneInfo)
117+
{
118+
#if NET8_0_OR_GREATER
119+
return TimeZoneInfo.TryFindSystemTimeZoneById(id, out timeZoneInfo);
120+
#else
121+
try {
122+
timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(id);
123+
return true;
124+
}
125+
catch {
126+
timeZoneInfo = null;
127+
return false;
128+
}
129+
#endif
69130
}
70131
}
71132
}
Lines changed: 115 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using Npgsql;
54
using NUnit.Framework;
65
using Xtensive.Sql.Drivers.PostgreSql;
6+
using PostgreSqlDriver = Xtensive.Sql.Drivers.PostgreSql.Driver;
77

88
namespace Xtensive.Orm.Tests.Sql.PostgreSql
99
{
1010
[TestFixture]
1111
public sealed class PostgreSqlHelperTest : SqlTest
1212
{
13-
private IReadOnlyDictionary<string, TimeSpan> serverTimeZones;
14-
15-
private string[] testTimezones;
13+
private string[] timezoneIdsWithWinAnalogue;
14+
private string[] timezoneIdsWithoutWinAnalogue;
1615

1716
public static TimeSpan[] Intervals
1817
{
@@ -44,61 +43,73 @@ public static TimeSpan[] Intervals
4443
};
4544
}
4645

47-
protected override void CheckRequirements() => Require.ProviderIs(StorageProvider.PostgreSql);
48-
49-
protected override void TestFixtureSetUp()
46+
public static string[] PosixOffsetFormatValues
5047
{
51-
base.TestFixtureSetUp();
52-
var nativeDriver = (Xtensive.Sql.Drivers.PostgreSql.Driver) Driver;
53-
54-
serverTimeZones = nativeDriver.PostgreServerInfo.ServerTimeZones;
55-
testTimezones = serverTimeZones.Keys.Union(new[] {
48+
get => new[] {
5649
"<+02>-02",
57-
"<+2>-2",
5850
"<+05>-05",
59-
"<+5>-5",
6051
"<+07>-07",
61-
"<+7>-7",
6252
"<-02>+02",
63-
"<-2>+2",
6453
"<-05>+05",
65-
"<-5>+5",
6654
"<-07>+07",
67-
"<-7>+7"
68-
} ).ToArray();
55+
"<-0730>+0730"
56+
};
57+
}
58+
59+
public static string[] PseudoPosixOffsetFormatValues
60+
{
61+
get => new[] {
62+
"<+2>-2",
63+
"<+5>-5",
64+
"<+7>-7",
65+
"<-2>+2",
66+
"<-5>+5",
67+
"<-7>+7",
68+
"<ulalala>not-ulalala"
69+
};
70+
}
71+
72+
protected override void CheckRequirements() => Require.ProviderIs(StorageProvider.PostgreSql);
73+
74+
protected override void TestFixtureSetUp()
75+
{
76+
base.TestFixtureSetUp();
77+
78+
LoadServerTimeZones(Connection, out timezoneIdsWithWinAnalogue, out timezoneIdsWithoutWinAnalogue);
6979

7080
Connection.Close();
7181
}
7282

7383
[Test]
74-
public void TimeZoneRecognitionTest()
84+
[TestCaseSource(nameof(PosixOffsetFormatValues))]
85+
public void PosixOffsetRecognitionTest(string offset)
7586
{
76-
foreach(var timezone in testTimezones) {
77-
78-
if (timezone.StartsWith('<')) {
79-
if (timezone.Contains('0')) {
80-
81-
82-
Assert.That(serverTimeZones.TryGetValue(timezone, out var result1), Is.False);
83-
Assert.That(serverTimeZones.TryGetValue(PostgreSqlHelper.TryGetZoneFromPosix(timezone), out var result2), Is.True);
84-
Assert.That(result2, Is.EqualTo(TimeSpan.FromHours(2))
85-
.Or.EqualTo(TimeSpan.FromHours(5))
86-
.Or.EqualTo(TimeSpan.FromHours(7))
87-
.Or.EqualTo(TimeSpan.FromHours(-2))
88-
.Or.EqualTo(TimeSpan.FromHours(-5))
89-
.Or.EqualTo(TimeSpan.FromHours(-7)));
90-
}
91-
else {
92-
Assert.That(serverTimeZones.TryGetValue(timezone, out var result1), Is.False);
93-
Assert.That(serverTimeZones.TryGetValue(PostgreSqlHelper.TryGetZoneFromPosix(timezone), out var result2), Is.False);
94-
}
87+
var systemTimezone = PostgreSqlHelper.GetTimeZoneInfoForServerTimeZone(offset);
88+
Assert.That(systemTimezone, Is.Not.Null);
89+
Assert.That(systemTimezone.Id.Contains("UTC"));
90+
}
9591

96-
}
97-
else {
98-
Assert.That(serverTimeZones.TryGetValue(timezone, out var result1), Is.True);
99-
Assert.That(serverTimeZones.TryGetValue(PostgreSqlHelper.TryGetZoneFromPosix(timezone), out var result2), Is.True);
100-
Assert.That(result1, Is.EqualTo(result2));
101-
}
92+
[Test]
93+
[TestCaseSource(nameof(PseudoPosixOffsetFormatValues))]
94+
public void PseudoPosixOffsetRecognitionTest(string offset)
95+
{
96+
var systemTimezone = PostgreSqlHelper.GetTimeZoneInfoForServerTimeZone(offset);
97+
Assert.That(systemTimezone, Is.Null);
98+
}
99+
100+
[Test]
101+
public void ResolvableTimeZonesTest()
102+
{
103+
foreach (var tz in timezoneIdsWithWinAnalogue) {
104+
Assert.That(PostgreSqlHelper.GetTimeZoneInfoForServerTimeZone(tz), Is.Not.Null);
105+
}
106+
}
107+
108+
[Test]
109+
public void UnresolvableTimeZonesTest()
110+
{
111+
foreach(var tz in timezoneIdsWithoutWinAnalogue) {
112+
Assert.That(PostgreSqlHelper.GetTimeZoneInfoForServerTimeZone(tz), Is.Null);
102113
}
103114
}
104115

@@ -110,5 +121,64 @@ public void TimeSpanToIntervalConversionTest(TimeSpan testValue)
110121
var backToTimeSpan = PostgreSqlHelper.ResurrectTimeSpanFromNpgsqlInterval(nativeInterval);
111122
Assert.That(backToTimeSpan, Is.EqualTo(testValue));
112123
}
124+
125+
126+
private static void LoadServerTimeZones(Xtensive.Sql.SqlConnection connection,
127+
out string[] timezoneIdsWithWinAnalogue,
128+
out string[] timezoneIdsWithoutWinAnalogue)
129+
{
130+
var timezoneIdsWithWinAnalogueList = new List<string>();
131+
var timezoneIdsWithoutWinAnalogueList = new List<string>();
132+
133+
var existing = new HashSet<string>();
134+
var serverTimeZoneAbbrevs = new HashSet<string>();
135+
using (var command = connection.CreateCommand("SELECT \"name\", \"abbrev\" FROM pg_catalog.pg_timezone_names"))
136+
using (var reader = command.ExecuteReader()) {
137+
while (reader.Read()) {
138+
var name = reader.GetString(0);
139+
var abbrev = reader.GetString(1);
140+
141+
if (name.Equals("ZULU", StringComparison.OrdinalIgnoreCase)
142+
|| abbrev.Equals("ZULU", StringComparison.OrdinalIgnoreCase))
143+
continue;
144+
145+
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(name, out var winAnalogue))
146+
timezoneIdsWithWinAnalogueList.Add(name);
147+
else
148+
timezoneIdsWithoutWinAnalogueList.Add(name);
149+
150+
if (abbrev[0] != '-' && abbrev[0] != '+' && existing.Add(abbrev)) {
151+
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(abbrev, out var winAnalogue1))
152+
timezoneIdsWithWinAnalogueList.Add(abbrev);
153+
else
154+
timezoneIdsWithoutWinAnalogueList.Add(abbrev);
155+
}
156+
}
157+
}
158+
159+
using (var command = connection.CreateCommand("SELECT \"abbrev\" FROM pg_catalog.pg_timezone_abbrevs"))
160+
using (var reader = command.ExecuteReader()) {
161+
while (reader.Read()) {
162+
var abbrev = reader.GetString(0);
163+
164+
if (abbrev.Equals("ZULU", StringComparison.OrdinalIgnoreCase) || !existing.Add(abbrev))
165+
continue;
166+
167+
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(abbrev, out var winAnalogue))
168+
timezoneIdsWithWinAnalogueList.Add(abbrev);
169+
else
170+
timezoneIdsWithoutWinAnalogueList.Add(abbrev);
171+
172+
if (abbrev[0] != '-' && abbrev[0] != '+' && existing.Add(abbrev)) {
173+
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(abbrev, out var winAnalogue1))
174+
timezoneIdsWithWinAnalogueList.Add(abbrev);
175+
else
176+
timezoneIdsWithoutWinAnalogueList.Add(abbrev);
177+
}
178+
}
179+
}
180+
timezoneIdsWithoutWinAnalogue = timezoneIdsWithoutWinAnalogueList.ToArray();
181+
timezoneIdsWithWinAnalogue = timezoneIdsWithWinAnalogueList.ToArray();
182+
}
113183
}
114184
}

0 commit comments

Comments
 (0)