//
// Copyright (c) 2016, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 26 Feb 2016 Matthew Giannini Creation
// 27 Jun 2023 Matthew Giannini Refactor for ES
//
**
** Tool for managing JS time zones.
**
class TzTool
{
//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////
new make(Str[] args := Env.cur.args)
{
this.args = args
}
private const Str[] args
private Log log := Log.get("TzTool")
// gen
private File js := Env.cur.homeDir + `etc/sys/fan_tz.js`
private File aliasProps := Env.cur.homeDir + `etc/sys/timezone-aliases.props`
private Str:Str aliases := [:]
private Str:TimeZone[] byContinent := [:]
//////////////////////////////////////////////////////////////////////////
// Run
//////////////////////////////////////////////////////////////////////////
Void run()
{
parseArgs
if (gen) generateTimeZones
}
//////////////////////////////////////////////////////////////////////////
// Gen
//////////////////////////////////////////////////////////////////////////
private Void generateTimeZones()
{
loadAliases
orderByContinent
writeTzJs
}
private Void loadAliases()
{
// load aliases
if (!aliasProps.exists) log.warn("$aliasProps does not exist")
else this.aliases = aliasProps.readProps
}
private Void orderByContinent()
{
TimeZone.listFullNames.each |fullName|
{
byContinent.getOrAdd(continent(fullName)) { [,] }.add(TimeZone.fromStr(fullName))
}
// sort time zones by city name
byContinent.vals.each { it.sort |a,b| { a.name <=> b.name } }
}
** Get the continent name from the full name, or ""
** if the full name doesn't have a continent.
private Str continent(Str fullName)
{
fullName.contains("/") ? fullName.split('/').first : ""
}
private Void writeTzJs()
{
jsOut := js.out
try
{
typeRef := this.embed ? "TimeZone" : "sys.TimeZone"
if (!embed)
{
jsOut.printLine("import * as sys from './sys.js'");
}
jsOut.printLine("const c=${typeRef}.__cache;")
// write built-in timezones
byContinent.each |TimeZone[] timezones, Str continent|
{
timezones.each |TimeZone tz|
{
log.debug("$tz.fullName")
encoded := encodeTimeZone(tz)
jsOut.printLine("c(${tz.fullName.toCode},${encoded.toBase64.toCode});")
}
}
// write aliases
jsOut.printLine("const a=${typeRef}.__alias;")
aliases.each |target, alias|
{
log.debug("Alias $alias = $target")
jsOut.printLine("a(${alias.toCode},${target.toCode});")
}
}
finally jsOut.close
log.info("Wrote: ${js.osPath ?: js}")
}
private Buf encodeTimeZone(TimeZone tz)
{
buf := Buf().writeUtf(tz.fullName);
rules := ([Str:Obj][])tz->rules
rules.each |r| { encodeRule(r, buf.out) }
return buf
}
private Void encodeRule(Str:Obj r, OutStream out)
{
dstOffset := r["dstOffset"]
out.writeI2(r["startYear"])
.writeI4(r["offset"])
.writeUtf(r["stdAbbr"])
.writeI4(dstOffset)
if (dstOffset != 0)
{
out.writeUtf(r["dstAbbr"])
encodeDst(r["dstStart"], out)
encodeDst(r["dstEnd"], out)
}
}
private Void encodeDst(Str:Obj dst, OutStream out)
{
out.write(dst["mon"])
.write(dst["onMode"])
.write(dst["onWeekday"])
.write(dst["onDay"])
.writeI4(dst["atTime"])
.write(dst["atMode"])
}
//////////////////////////////////////////////////////////////////////////
// Args
//////////////////////////////////////////////////////////////////////////
private Bool gen := false
private Bool embed := false
private Void parseArgs()
{
if (args.isEmpty) usage()
i :=0
while (i < args.size)
{
arg := args[i++]
switch (arg)
{
case "-gen":
this.gen = true
case "-embed":
this.embed = true
case "-outDir":
outDir := args[i++].toUri.toFile
if (!outDir.isDir) throw ArgErr("Not a directory: ${outDir}")
this.js = outDir.plus(`fan_tz.js`)
case "-silent":
this.log.level = LogLevel.silent
case "-verbose":
case "-v":
log.level = LogLevel.debug
case "-help":
case "-?":
usage()
default:
Env.cur.err.printLine("Bad option: ${arg}")
usage()
}
}
}
private Void usage()
{
out := Env.cur.out
main := Env.cur.mainMethod?.parent?.name ?: "TzTool"
out.printLine(
"Usage:
$main [options]
Options:
-gen Generate fan_tz.js
-embed Generate code to be embedded directly in sys.js
-outDir (optional) generate fan_tz.js in this directory
-verbose, -v Enable verbose logging
-silent Suppress all logging
-help, -? Print usage help
")
Env.cur.exit(1)
}
//////////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////////
static Void main() { TzTool().run }
}