blob: 9392585e6c3557a5f0bbe755f5863399e78a94c5 [file] [edit]
/// A tool to download and compile TimeZone database
///
/// Usage example:
///
/// ```sh
/// dart run tool/get
/// dart run tool/encode_dart lib/data/latest.{tzf,dart}
/// dart run tool/encode_dart lib/data/latest_all.{tzf,dart}
/// dart run tool/encode_dart lib/data/latest_10y.{tzf,dart}
/// ```
library;
import 'dart:async';
import 'dart:io';
import 'package:args/args.dart';
import 'package:file/file.dart' as pkg_file;
import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:timezone/src/common_locations.dart' show commonLocations;
import 'package:timezone/src/tools.dart';
import 'package:timezone/src/tzdb.dart';
import 'package:timezone/timezone.dart';
import 'package:timezone/tzdata.dart' as tzfile;
final outPath = p.join('lib', 'data');
const _zicDataFiles = [
'africa',
'antarctica',
'asia',
'australasia',
'etcetera',
'europe',
'northamerica',
'southamerica',
'backward',
];
const _repositoryUri = 'https://data.iana.org/time-zones';
/// Load [tzfile.Location] from tzfile.
Future<tzfile.Location> loadTzfileLocation(String name, String path) async {
final rawData = await File(path).readAsBytes();
return tzfile.Location.fromBytes(name, rawData);
}
/// Download IANA Time Zone database to [dest] directory.
Future<String> downloadTzData(String version, String dest) async {
final outPath = p.join(dest, 'tzdata$version.tar.gz');
final client = HttpClient();
try {
final uri = version == 'latest'
? Uri.parse('$_repositoryUri/tzdata-$version.tar.gz')
: Uri.parse('$_repositoryUri/releases/tzdata$version.tar.gz');
final req = await client.getUrl(uri);
final resp = await req.close();
final out = File(outPath);
final sink = out.openWrite();
try {
await sink.addStream(resp);
} finally {
await sink.close();
}
} finally {
client.close();
}
return outPath;
}
/// Unpack IANA Time Zone database to [dest] directory.
Future<bool> unpackTzData(String archivePath, String dest) async {
final result = await Process.run('tar', [
'--directory=$dest',
'-zxf',
archivePath,
]);
if (result.exitCode == 0) {
return true;
}
throw Exception(result.stderr);
}
/// Compile IANA Time Zone file with zic compiler.
Future<bool> zic(String src, String dest) async {
final result = await Process.run('zic', ['-d', dest, src]);
if (result.exitCode == 0) {
return true;
}
throw Exception(result.stderr);
}
Future<void> main(List<String> arguments) 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');
// Parse CLI arguments
final parser = ArgParser()
..addOption('source', abbr: 's', defaultsTo: 'latest');
final argResults = parser.parse(arguments);
final source = argResults['source'] as String?;
if (source == null || source.isEmpty) {
print(parser.usage);
exit(64);
}
final tzfileLocations = <tzfile.Location>[];
log.info('Creating temp directory');
final tmpDir = await Directory.systemTemp.createTemp('tzdata-');
log.info('Temp directory created: ${tmpDir.path}');
try {
log.info('Downloading timezone data');
final archivePath = await downloadTzData(source /*!*/, tmpDir.path);
log.info('Unpacking timezone data: $archivePath');
await unpackTzData(archivePath, tmpDir.path);
log.info('Creating zic directory');
final zicDirPath = p.join(tmpDir.path, 'zic');
await Directory(zicDirPath).create();
log.info('Compiling timezone data with zic compiler');
for (final i in _zicDataFiles) {
log.info('- $i');
await zic(p.join(tmpDir.path, i), zicDirPath);
}
log.info('Importing tzfile Locations');
final files = await Glob('**/*').list(root: zicDirPath).toList();
for (final f in files) {
if (f is pkg_file.File) {
final name = p.relative(f.path, from: zicDirPath);
log.info('- $name');
final loc = await loadTzfileLocation(name, f.path);
tzfileLocations.add(loc);
}
}
} finally {
log.info('Cleaning up temporary directories');
await tmpDir.delete(recursive: true);
}
log.info('Converting tzfile Locations to native Locations');
final db = LocationDatabase();
for (final loc in tzfileLocations) {
db.add(tzfileLocationToNativeLocation(loc));
}
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 common10yDb = filterTimeZoneData(
commonDb.db,
dateFrom: DateTime(DateTime.now().year - 5, 1, 1).millisecondsSinceEpoch,
dateTo: DateTime(DateTime.now().year + 5, 1, 1).millisecondsSinceEpoch,
locations: commonLocations,
);
logReport(common10yDb.report);
log.info('Serializing location databases');
final allOut = File(p.join(outPath, '${source}_all.tzf'));
final commonOut = File(p.join(outPath, '$source.tzf'));
final common10yOut = File(p.join(outPath, '${source}_10y.tzf'));
await allOut.writeAsBytes(tzdbSerialize(allDb.db), flush: true);
await commonOut.writeAsBytes(tzdbSerialize(commonDb.db), flush: true);
await common10yOut.writeAsBytes(tzdbSerialize(common10yDb.db), flush: true);
exit(0);
}