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
5 changes: 5 additions & 0 deletions bin/refresh.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import '../tool/refresh.dart' as runner;

Future<void> main(List<String> args) async {
await runner.main(args);
}
2 changes: 1 addition & 1 deletion lib/src/env.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const String tzDataDefaultFilename = 'latest.tzf';
final _UTC = Location('UTC', [minTime], [0], [TimeZone.UTC]);

final _database = LocationDatabase();
late Location _local;
Location _local = _UTC;

/// Global TimeZone database
LocationDatabase get timeZoneDatabase => _database;
Expand Down
46 changes: 37 additions & 9 deletions lib/src/location.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,55 @@ class Location {
// since January 1, 1970 UTC, to match the argument
// to lookup.
static final int _cacheNow = DateTime.now().millisecondsSinceEpoch;
int _cacheStart = 0;
int _cacheEnd = 0;
late TimeZone _cacheZone;

Location(this.name, this.transitionAt, this.transitionZone, this.zones) {
final int _cacheStart;
final int _cacheEnd;
final TimeZone _cacheZone;

Location._(
this.name,
this.transitionAt,
this.transitionZone,
this.zones,
this._cacheStart,
this._cacheEnd,
this._cacheZone,
);

factory Location(
String name,
List<int> transitionAt,
List<int> transitionZone,
List<TimeZone> zones,
) {
// Fill in the cache with information about right now,
// since that will be the most common lookup.
int cacheStart = 0;
int cacheEnd = maxTime;
TimeZone cacheZone = TimeZone.UTC; // fallback

for (var i = 0; i < transitionAt.length; i++) {
final tAt = transitionAt[i];

if ((tAt <= _cacheNow) &&
((i + 1 == transitionAt.length) ||
(_cacheNow < transitionAt[i + 1]))) {
_cacheStart = tAt;
_cacheEnd = maxTime;
cacheStart = tAt;
cacheEnd = maxTime;
if (i + 1 < transitionAt.length) {
_cacheEnd = transitionAt[i + 1];
cacheEnd = transitionAt[i + 1];
}
_cacheZone = zones[transitionZone[i]];
cacheZone = zones[transitionZone[i]];
}
}
return Location._(
name,
transitionAt,
transitionZone,
zones,
cacheStart,
cacheEnd,
cacheZone,
);
}

/// translate instant in time expressed as milliseconds since
Expand Down
3 changes: 3 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ dev_dependencies:
lints: ^3.0.0
logging: ^1.2.0
test: ^1.16.0

executables:
refresh: bin/refresh.dart
12 changes: 10 additions & 2 deletions tool/encode_dart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ Future<void> main(List<String> args) async {
File(dartLibraryPath).writeAsStringSync(generatedDartFile);
}

Future<void> encodeDart(String tzDataPath, String filePath) async {
final bytes = File(tzDataPath).readAsBytesSync();
final generatedDartFile = generateDartFile(
name: p.basenameWithoutExtension(tzDataPath),
data: bytesAsString(bytes),
);
File(filePath).writeAsStringSync(generatedDartFile);
}

String bytesAsString(Uint8List bytes) {
assert(bytes.length.isEven);
return bytes.buffer
Expand All @@ -26,8 +35,7 @@ String generateDartFile({required String name, required String data}) =>
'''// This is a generated file. Do not edit.
import 'dart:typed_data';

import 'package:timezone/src/env.dart';
import 'package:timezone/src/exceptions.dart';
import 'package:timezone/timezone.dart';

/// Initialize Time Zone database from $name.
///
Expand Down
57 changes: 57 additions & 0 deletions tool/encode_tzf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,60 @@ Future<void> main(List<String> arguments) async {
await write(args['output-common'] as String, commonDb.db);
await write(args['output-10y'] as String, common_10y_Db.db);
}

Future<void> encodeTzf(
{required String zoneInfoPath,
String outputAll = 'lib/data/latest_all.tzf',
String outputCommon = 'lib/data/latest.tzf',
String output10y = 'lib/data/latest_10y.tzf'}) async {
// Initialize logger
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((LogRecord rec) {
print('${rec.level.name}: ${rec.time}: ${rec.message}');
});
final log = Logger('main');

final db = LocationDatabase();

log.info('Importing zoneinfo files');
final files = await Glob('**').list(root: zoneInfoPath).toList();
for (final f in files) {
if (f is pkg_file.File) {
final name = p.relative(f.path, from: zoneInfoPath).replaceAll('\\', '/');
log.info('- $name');
db.add(tzfileLocationToNativeLocation(
tzfile.Location.fromBytes(name, await f.readAsBytes())));
}
}

void logReport(FilterReport r) {
log.info(' + locations: ${r.originalLocationsCount} => '
'${r.newLocationsCount}');
log.info(' + transitions: ${r.originalTransitionsCount} => '
'${r.newTransitionsCount}');
}

log.info('Building location databases:');

log.info('- all locations');
final allDb = filterTimeZoneData(db);
logReport(allDb.report);

log.info('- common locations from all locations');
final commonDb = filterTimeZoneData(allDb.db, locations: commonLocations);
logReport(commonDb.report);

log.info('- [+- 5 years] from common locations');
final common_10y_Db = filterTimeZoneData(commonDb.db,
dateFrom: DateTime(DateTime.now().year - 5, 1, 1).millisecondsSinceEpoch,
dateTo: DateTime(DateTime.now().year + 5, 1, 1).millisecondsSinceEpoch,
locations: commonLocations);
logReport(common_10y_Db.report);

log.info('Serializing location databases');
Future<void> write(String file, LocationDatabase db) =>
File(file).writeAsBytes(tzdbSerialize(db), flush: true);
await write(outputAll, allDb.db);
await write(outputCommon, commonDb.db);
await write(output10y, common_10y_Db.db);
}
191 changes: 191 additions & 0 deletions tool/refresh.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import 'dart:io';
import 'package:args/args.dart';

import 'encode_dart.dart' as encode_dart;
import 'encode_tzf.dart' as encode_tzf;

const String _defaultSourceUrl =
"https://data.iana.org/time-zones/tzdata-latest.tar.gz";

const String _defaultFilePrefix = "latest";

Future<void> main(List<String> args) async {
try {
await _checkCommand('curl');
await _checkCommand('tar');
await _checkCommand('make');
await _checkCommand('zic');
} on Exception catch (e) {
print(e);
return;
}

final parser = ArgParser()
..addOption('output',
abbr: 'o', help: 'Output directory)', defaultsTo: 'lib/data')
..addOption('source',
abbr: 's',
help: 'Source URL for timezone data',
defaultsTo: _defaultSourceUrl)
..addOption('file-prefix',
abbr: 'p',
help:
// ignore: lines_longer_than_80_chars
'Prefix for the generated files. E.g.: "latest", "2025", etc.\nRequired when passing a custom source',
defaultsTo: _defaultFilePrefix)
..addFlag('help', abbr: 'h', help: 'Show help information');

final argResults = parser.parse(args);

if (argResults['help'] == true) {
print(
"\nWELCOME TO TIMEZONE DATA GENERATOR\n\nThis utility is used to generate/regenerate timezone files (*.tzf/*.dart) from IANA timezone data archives. See https://data.iana.org/time-zones.\n\nIMPORTANT NOTE: This utility only works on Linux and Unix-like systems due its dependence on ZIC utility. So If you are using Windows, please run this in a WSL environment.\n\nOptions:\n");
print('Timezone Data Generator Tool');
print(parser.usage);
return;
}

final sourceURL = argResults['source'] as String;
final filePrefix = argResults['file-prefix'] as String;

if (sourceURL != _defaultSourceUrl && filePrefix == _defaultFilePrefix) {
print(
// ignore: lines_longer_than_80_chars
"Error: When using a custom source URL, you must also provide a custom --file-prefix to avoid overwriting default files.");
return;
}

final outputPath = argResults['output'] as String;
final outputDir = Directory(outputPath);

await outputDir.create(recursive: true);
print('Writing output to: ${outputDir.absolute.path}');

final tmpDir = await _makeTempDirectory();

try {
await _downloadAndExtractTarGz(Uri.parse(sourceURL), tmpDir);
await runMake(tmpDir);
await runZic(tmpDir);

await runEncodeTzf(filePrefix, '${tmpDir.path}/zoneinfo', outputDir.path);
await runEmbedScopes(filePrefix, outputDir.path);
await formatDartFiles(outputDir.path);
} finally {
print('Cleaning up temp files...');
await tmpDir.delete(recursive: true);
}

print('Done!');
}

Future<Directory> _makeTempDirectory() async {
final tempDir =
Directory('__tmp__${DateTime.now().microsecondsSinceEpoch}__tz__');
var exists = await tempDir.exists();
if (exists) {
return _makeTempDirectory();
}
return tempDir;
}

Future<void> _downloadAndExtractTarGz(Uri url, Directory outputDir) async {
await outputDir.create(recursive: true);

final curl = await Process.start('curl', ['-sL', url.toString()]);
final tar = await Process.start('tar', ['-zx', '-C', outputDir.path]);

// Pipe curl stdout to tar stdin
await curl.stdout.pipe(tar.stdin);

curl.stderr.transform(SystemEncoding().decoder).listen(stderr.write);
tar.stderr.transform(SystemEncoding().decoder).listen(stderr.write);

final curlExit = await curl.exitCode;
final tarExit = await tar.exitCode;

if (curlExit != 0 || tarExit != 0) {
throw Exception(
'Failed to download and extract. Exit codes: curl=$curlExit, tar=$tarExit');
}

print('Extracted tzdata to ${outputDir.path}');
}

Future<void> formatDartFiles(String outputPath) async {
print('Formatting Dart files in $outputPath...');
final result = await Process.run('dart', ['format', outputPath]);

if (result.exitCode != 0) {
print('Formatting failed:\n${result.stderr}');
throw Exception('dart format failed');
}

print('Formatting complete');
}

Future<void> runEmbedScopes(String filePrefix, String outputPath) async {
final scopes = [filePrefix, '${filePrefix}_all', '${filePrefix}_10y'];

for (final scope in scopes) {
final tzfPath = '$outputPath/$scope.tzf';
final dartPath = '$outputPath/$scope.dart';

print('Creating embedding: $scope...');
await encode_dart.encodeDart(tzfPath, dartPath);
print('Created: $dartPath');
}
}

Future<void> runEncodeTzf(
String filePrefix, String zoneInfoPath, String outputPath) async {
print('Running encode_tzf.dart...');
await encode_tzf.encodeTzf(
zoneInfoPath: zoneInfoPath,
outputCommon: '$outputPath/$filePrefix.tzf',
outputAll: '$outputPath/${filePrefix}_all.tzf',
output10y: '$outputPath/${filePrefix}_10y.tzf',
);
}

Future<void> runZic(Directory dir) async {
print('Running zic...');
final zoneInfoDir = Directory('${dir.path}/zoneinfo');
await zoneInfoDir.create();

final result = await Process.run(
'zic',
['-d', zoneInfoDir.absolute.path, '-b', 'fat', 'rearguard.zi'],
workingDirectory: dir.path,
);

if (result.exitCode != 0) {
print('zic failed:\n${result.stderr}');
throw Exception('zic failed');
}

print('zic compilation complete');
}

Future<void> runMake(Directory dir) async {
print('Running make rearguard.zi...');
final result = await Process.run(
'make',
['rearguard.zi'],
workingDirectory: dir.path,
);

if (result.exitCode != 0) {
print('make failed:\n${result.stderr}');
throw Exception('make failed');
}

print('make rearguard.zi succeeded');
}

Future<void> _checkCommand(String cmd) async {
final result = await Process.run('which', [cmd]);
if (result.exitCode != 0) {
throw Exception('Required command `$cmd` not found. Please install it.');
}
}