Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions TimeZoneNames.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimeZoneNames.DataBuilder",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimeZoneNames.Tests", "test\TimeZoneNames.Tests\TimeZoneNames.Tests.csproj", "{3CE7A88C-99D1-4740-BEEB-0F88E3B9A2F3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimeZoneNames.Generators", "src\TimeZoneNames.Generators\TimeZoneNames.Generators.csproj", "{F63292AE-6ED4-45A7-A9C9-D31331301AD0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -27,6 +29,10 @@ Global
{3CE7A88C-99D1-4740-BEEB-0F88E3B9A2F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3CE7A88C-99D1-4740-BEEB-0F88E3B9A2F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3CE7A88C-99D1-4740-BEEB-0F88E3B9A2F3}.Release|Any CPU.Build.0 = Release|Any CPU
{F63292AE-6ED4-45A7-A9C9-D31331301AD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F63292AE-6ED4-45A7-A9C9-D31331301AD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F63292AE-6ED4-45A7-A9C9-D31331301AD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F63292AE-6ED4-45A7-A9C9-D31331301AD0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"version": "10.0.100",
"rollForward": "latestMinor"
}
}
6 changes: 2 additions & 4 deletions src/TimeZoneNames.DataBuilder/DataExtractor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.IO.Compression;
using System.Text.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Xml.Linq;
using System.Xml.XPath;
Expand Down Expand Up @@ -33,8 +32,7 @@ public static DataExtractor Load(string dataPath, bool overwrite)
public void SaveData(string outputFilePath)
{
using var stream = File.Create(outputFilePath);
using var compressedStream = new GZipStream(stream, CompressionLevel.Optimal);
JsonSerializer.Serialize(compressedStream, _data);
JsonSerializer.Serialize(stream, _data);
}

private TzdbDateTimeZoneSource _tzdbSource = null!;
Expand Down
2 changes: 1 addition & 1 deletion src/TimeZoneNames.DataBuilder/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ static void Main()

var extractor = DataExtractor.Load("data", overwrite: false);

var dataFileName = "data.json.gz";
var dataFileName = "data.json";
var outputFilePath = Path.Combine("data", dataFileName);
extractor.SaveData(outputFilePath);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down
169 changes: 169 additions & 0 deletions src/TimeZoneNames.Generators/TimeZoneDataGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using System.Globalization;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace TimeZoneNames.Generators;

/// <summary>
/// Encodes data.json into the byte array that TimeZoneData.Load() decodes, in the order Load() reads it.
/// A byte array initializer compiles to a data blob rather than to IL, so the JIT has nothing large to compile.
/// </summary>
[Generator]
public class TimeZoneDataGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var source = context.AdditionalTextsProvider
.Where(file => Path.GetFileName(file.Path) == "data.json")
.Select((file, ct) => Generate(file.GetText(ct)!.ToString()));

context.RegisterSourceOutput(source, (spc, code) => spc.AddSource("TimeZoneData.g.cs", SourceText.From(code, Encoding.UTF8)));
}

private static string Generate(string json)
{
using var reader = new JsonTextReader(new StringReader(json)) { DateParseHandling = DateParseHandling.None };
var data = JObject.Load(reader);
var writer = new Writer();

writer.Dictionary(data["TzdbZoneCountries"], writer.Array);
writer.Dictionary(data["CldrZoneCountries"], writer.Array);
writer.Dictionary(data["CldrMetazones"], writer.String);
writer.Dictionary(data["CldrPrimaryZones"], writer.String);
writer.Dictionary(data["CldrAliases"], writer.String);
writer.Dictionary(data["CldrLanguageData"], language =>
{
writer.NullableValues(language["Formats"]);
writer.String(language["FallbackFormat"]);
writer.Dictionary(language["ShortNames"], writer.Values);
writer.Dictionary(language["LongNames"], writer.Values);
writer.Dictionary(language["CountryNames"], writer.String);
writer.Dictionary(language["CityNames"], writer.String);
});

var zones = (JArray)data["SelectionZones"]!;
writer.Int(zones.Count);
foreach (var zone in zones)
{
writer.String(zone["Id"]);
writer.Int(DateTime.Parse((string)zone["ThresholdUtc"]!, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind).Ticks);
}

writer.Dictionary(data["DisplayNames"], names => writer.Dictionary(names, writer.String));

var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("namespace TimeZoneNames;");
sb.AppendLine();
sb.AppendLine("internal partial class TimeZoneData");
sb.AppendLine("{");
sb.Append(" private static byte[] Blob() => new byte[] { ");
foreach (var b in writer.ToArray())
{
sb.Append(b).Append(',');
}
sb.AppendLine(" };");
sb.AppendLine("}");
return sb.ToString();
}

/// <summary>
/// Writes each string as an index into a pool of distinct strings, so Load() creates every string once.
/// </summary>
private sealed class Writer
{
private readonly MemoryStream _body = new();
private readonly Dictionary<string, int> _ids = new(StringComparer.Ordinal);
private readonly List<string> _pool = new();

public void Int(long value) => Int(_body, value);

public void String(JToken? token)
{
if (token is not { Type: JTokenType.String })
{
Int(0);
return;
}

var value = (string)token!;
if (!_ids.TryGetValue(value, out var id))
{
_pool.Add(value);
id = _pool.Count;
_ids.Add(value, id);
}

Int(id);
}

public void Array(JToken token)
{
var array = (JArray)token;
Int(array.Count);
foreach (var item in array)
{
String(item);
}
}

public void Dictionary(JToken? token, Action<JToken> value)
{
var properties = ((JObject)token!).Properties().ToList();
Int(properties.Count);
foreach (var property in properties)
{
String(property.Name);
value(property.Value);
}
}

public void Values(JToken token)
{
String(token["Generic"]);
String(token["Standard"]);
String(token["Daylight"]);
}

public void NullableValues(JToken? token)
{
if (token is not JObject values)
{
Int(0);
return;
}

Int(1);
Values(values);
}

public byte[] ToArray()
{
var output = new MemoryStream();
Int(output, _pool.Count);
foreach (var value in _pool)
{
var bytes = Encoding.UTF8.GetBytes(value);
Int(output, bytes.Length);
output.Write(bytes, 0, bytes.Length);
}

_body.WriteTo(output);
return output.ToArray();
}

private static void Int(Stream stream, long value)
{
while (value >= 0x80)
{
stream.WriteByte((byte)(value | 0x80));
value >>= 7;
}

stream.WriteByte((byte)value);
}
}
}
26 changes: 26 additions & 0 deletions src/TimeZoneNames.Generators/TimeZoneNames.Generators.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup>

<!-- The compiler loads analyzer dependencies only from next to the analyzer, so ship Newtonsoft.Json with it. -->
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>

<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

</Project>
119 changes: 119 additions & 0 deletions src/TimeZoneNames/TimeZoneData.Load.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Text;

namespace TimeZoneNames;

internal partial class TimeZoneData
{
public static TimeZoneData Load()
{
var reader = new Reader(Blob());
return new TimeZoneData
{
TzdbZoneCountries = reader.Dictionary(reader.Array),
CldrZoneCountries = reader.Dictionary(reader.Array),
CldrMetazones = reader.Dictionary(reader.String),
CldrPrimaryZones = reader.Dictionary(reader.String),
CldrAliases = reader.Dictionary(reader.String),
CldrLanguageData = reader.Dictionary(() => new CldrLanguageData
{
Formats = reader.NullableValues(),
FallbackFormat = reader.NullableString(),
ShortNames = reader.Dictionary(reader.Values),
LongNames = reader.Dictionary(reader.Values),
CountryNames = reader.Dictionary(reader.String),
CityNames = reader.Dictionary(reader.String)
}),
SelectionZones = reader.List(() => new TimeZoneSelectionData
{
Id = reader.String(),
ThresholdUtc = new DateTime(reader.Long(), DateTimeKind.Utc)
}),
DisplayNames = reader.Dictionary<IDictionary<string, string>>(() => reader.Dictionary(reader.String, StringComparer.Ordinal))
};
}

private sealed class Reader
{
private readonly byte[] _data;
private readonly string?[] _strings;
private int _position;

public Reader(byte[] data)
{
_data = data;
_strings = new string?[Int() + 1];
for (var i = 1; i < _strings.Length; i++)
{
var length = Int();
_strings[i] = Encoding.UTF8.GetString(_data, _position, length);
_position += length;
}
}

public long Long()
{
long value = 0;
for (var shift = 0; ; shift += 7)
{
var b = _data[_position++];
value |= (long)(b & 0x7F) << shift;
if (b < 0x80)
{
return value;
}
}
}

public int Int() => (int)Long();

public string? NullableString() => _strings[Int()];

public string String() => NullableString() ?? throw new InvalidDataException("Unexpected null string in time zone data.");

public string[] Array()
{
var array = new string[Int()];
for (var i = 0; i < array.Length; i++)
{
array[i] = String();
}

return array;
}

public TimeZoneValues Values() => new()
{
Generic = NullableString(),
Standard = NullableString(),
Daylight = NullableString()
};

public TimeZoneValues? NullableValues() => Int() == 0 ? null : Values();

public List<T> List<T>(Func<T> item)
{
var count = Int();
var list = new List<T>(count);
for (var i = 0; i < count; i++)
{
list.Add(item());
}

return list;
}

public Dictionary<string, T> Dictionary<T>(Func<T> value) => Dictionary(value, StringComparer.OrdinalIgnoreCase);

public Dictionary<string, T> Dictionary<T>(Func<T> value, StringComparer comparer)
{
var count = Int();
var dictionary = new Dictionary<string, T>(count, comparer);
for (var i = 0; i < count; i++)
{
dictionary.Add(String(), value());
}

return dictionary;
}
}
}
Loading