webtools/cmds/timestamps/main.go

201 lines
6.5 KiB
Go

//go:build js
package main
import (
"strconv"
"strings"
"syscall/js"
"time"
)
func main() {
c := make(chan struct{}, 0)
doc := js.Global().Get("document")
inputArea := doc.Call("getElementById", "input")
outputArea := doc.Call("getElementById", "output")
operationSelect := doc.Call("getElementById", "operation")
timezoneRadios := doc.Call("getElementsByName", "timezone")
var updateOutput js.Func
updateOutput = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
operation := operationSelect.Get("value").String()
timezone := "utc"
for i := 0; i < timezoneRadios.Length(); i++ {
if timezoneRadios.Index(i).Get("checked").Bool() {
timezone = timezoneRadios.Index(i).Get("value").String()
break
}
}
lines := strings.Split(inputArea.Get("value").String(), "\n")
var processedLines []string
for _, line := range lines {
processedLines = append(processedLines, processLine(line, operation, timezone))
}
outputArea.Set("value", strings.Join(processedLines, "\n"))
return nil
})
inputArea.Call("addEventListener", "input", updateOutput)
operationSelect.Call("addEventListener", "change", updateOutput)
for i := 0; i < timezoneRadios.Length(); i++ {
timezoneRadios.Index(i).Call("addEventListener", "change", updateOutput)
}
// Initial sync
updateOutput.Invoke()
<-c
}
func processLine(line, operation, timezone string) string {
trimmedLine := strings.TrimSpace(line)
if trimmedLine == "" || strings.HasPrefix(trimmedLine, "#") {
return line
}
var t time.Time
var err error
switch operation {
case "unix":
sec, err := strconv.ParseInt(trimmedLine, 10, 64)
if err != nil {
return "Invalid Date"
}
t = time.Unix(sec, 0)
case "unix_micro":
ms, err := strconv.ParseInt(trimmedLine, 10, 64)
if err != nil {
return "Invalid Date"
}
t = time.Unix(0, ms*1000000)
case "to_utc", "from_utc":
// Go's time.Parse is strict. We need to handle ISO 8601 flexible formats.
// Try standard layouts.
layouts := []string{
time.RFC3339,
time.RFC3339Nano,
"2006-01-02T15:04:05",
"2006-01-02T15:04:05.999",
}
parsed := false
for _, layout := range layouts {
t, err = time.Parse(layout, trimmedLine)
if err == nil {
parsed = true
break
}
}
if !parsed {
return "Invalid Date"
}
}
if operation == "to_utc" {
// If input has no timezone (t.Location() is UTC but offset is 0 and it might have been local),
// time.Parse("2006-01-02T15:04:05", ...) returns UTC time with that face value.
// If we want to treat it as "Local" (browser local), we don't know what browser local is easily in Go WASM without JS help.
// However, the JS implementation relied on `new Date()` which uses browser local.
// In Go WASM, `time.Local` is usually UTC unless configured.
// Let's use JS Date for parsing to be consistent with previous behavior and browser's local time?
// Or we can try to implement it purely in Go.
// If we want to support "Local" interpretation of a string like "2023-01-01T10:00:00", we need the local offset.
// We can get the local timezone offset from JS.
if timezone == "utc" {
// If user selected UTC, we treat the input as UTC (which time.Parse does by default for no-offset strings)
// and output UTC.
return t.UTC().Format("2006-01-02T15:04:05.000Z07:00")
} else {
// If user selected Local, we treat input as Local?
// "To UTC": converts the input in ISO 8601 in the selected timezone into UTC.
// If I select "Local" and input "10:00", I mean "10:00 Local". I want "XX:XX UTC".
// If the input string HAS an offset, we respect it.
// If it doesn't, we assume it's in `timezone`.
// Check if input has offset.
// time.Parse parses into UTC if no offset found.
// If we want to interpret "10:00" as Local, we need to adjust.
// Let's stick to a simpler approach: Use JS to parse if possible, or just use Go.
// Using JS from Go is easy.
jsDate := js.Global().Get("Date").New(trimmedLine)
if jsDate.Call("toString").String() == "Invalid Date" {
return "Invalid Date"
}
// If "to_utc", we just want to show the UTC string.
// But wait, `new Date("2023-01-01T10:00:00")` is Local in browser.
// `new Date("2023-01-01T10:00:00Z")` is UTC.
if timezone == "utc" && !strings.HasSuffix(strings.ToUpper(trimmedLine), "Z") && !strings.ContainsAny(trimmedLine, "+-") {
// Force UTC interpretation
jsDate = js.Global().Get("Date").New(trimmedLine + "Z")
}
return jsDate.Call("toISOString").String()
}
}
if operation == "from_utc" {
// Input assumed UTC.
// "2023-01-01T10:00:00" -> Treat as UTC.
jsDate := js.Global().Get("Date").New(trimmedLine)
if !strings.HasSuffix(strings.ToUpper(trimmedLine), "Z") && !strings.ContainsAny(trimmedLine, "+-") {
jsDate = js.Global().Get("Date").New(trimmedLine + "Z")
}
if jsDate.Call("toString").String() == "Invalid Date" {
return "Invalid Date"
}
if timezone == "utc" {
return jsDate.Call("toISOString").String()
} else {
// Local
// We want to display in local time.
// JS `toISOString` is always UTC.
// We can construct a local ISO-like string.
// Or use `toLocaleString`? But requirement says "ISO 8601".
// Let's do the offset trick.
offset := jsDate.Call("getTimezoneOffset").Int() // minutes
// Subtract offset (which is positive for West, negative for East usually? Wait. MDN: UTC - Local in minutes. So -480 for SG.)
// Actually MDN: "The time-zone offset is the difference, in minutes, between UTC and local time. Note that this means that the offset is positive if the local timezone is behind UTC and negative if it is ahead."
// So SG (UTC+8) is -480.
// To get local time in UTC components: add (-offset).
// timestamp - (offset * 60 * 1000)
newTime := jsDate.Call("getTime").Float() - (float64(offset) * 60 * 1000)
localDate := js.Global().Get("Date").New(newTime)
iso := localDate.Call("toISOString").String()
return strings.TrimSuffix(iso, "Z")
}
}
// Unix / Unix Micro
// t is the time.
if timezone == "utc" {
return t.UTC().Format("2006-01-02T15:04:05.000Z07:00")
} else {
// Local
// We need browser's local timezone.
// Go's `time.Local` is not browser's local.
// Use JS.
jsDate := js.Global().Get("Date").New(t.UnixMilli())
offset := jsDate.Call("getTimezoneOffset").Int()
newTime := jsDate.Call("getTime").Float() - (float64(offset) * 60 * 1000)
localDate := js.Global().Get("Date").New(newTime)
iso := localDate.Call("toISOString").String()
return strings.TrimSuffix(iso, "Z")
}
}