Vibe coded "timestamps"

This commit is contained in:
Leon Mika 2025-11-19 21:20:06 +11:00
parent 35270f72ca
commit 7bf586757a
6 changed files with 441 additions and 0 deletions

View file

@ -13,6 +13,7 @@ build.wasm:
mkdir target/wasm mkdir target/wasm
GOOS=js GOARCH=wasm go build -o target/wasm/clocks.wasm ./cmds/clocks GOOS=js GOARCH=wasm go build -o target/wasm/clocks.wasm ./cmds/clocks
GOOS=js GOARCH=wasm go build -o target/wasm/gotemplate.wasm ./cmds/gotemplate GOOS=js GOARCH=wasm go build -o target/wasm/gotemplate.wasm ./cmds/gotemplate
GOOS=js GOARCH=wasm go build -o target/wasm/timestamps.wasm ./cmds/timestamps
cp $(GOROOT)/lib/wasm/wasm_exec.js target/wasm/. cp $(GOROOT)/lib/wasm/wasm_exec.js target/wasm/.
.Phony: build.site .Phony: build.site

200
cmds/timestamps/main.go Normal file
View file

@ -0,0 +1,200 @@
//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")
}
}

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Timestamp Converter</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>Timestamp Converter</h1>
<div class="controls">
<select id="operation">
<option value="unix">Convert from Unix</option>
<option value="unix_micro">Convert from Unix Micro</option>
<option value="to_utc">To UTC</option>
<option value="from_utc">From UTC</option>
</select>
<div class="timezone-selector">
<label>
<input type="radio" name="timezone" value="utc" checked> UTC
</label>
<label>
<input type="radio" name="timezone" value="local"> Local
</label>
</div>
</div>
<div class="editor-pane">
<textarea id="input" placeholder="Enter timestamps here (one per line)..." spellcheck="false"></textarea>
<textarea id="output" readonly placeholder="Results will appear here..." spellcheck="false"></textarea>
</div>
</div>
<script src="main.js" type="module"></script>
</body>
</html>

8
site/timestamps/main.js Normal file
View file

@ -0,0 +1,8 @@
import "/wasm/wasm_exec.js";
const go = new Go();
WebAssembly.instantiateStreaming(fetch("/wasm/timestamps.wasm"), go.importObject)
.then((result) => {
go.run(result.instance);
});

91
site/timestamps/style.css Normal file
View file

@ -0,0 +1,91 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
h1 {
margin-top: 0;
margin-bottom: 20px;
font-size: 24px;
color: #333;
}
.controls {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 20px;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
select {
padding: 8px 12px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
}
.timezone-selector {
display: flex;
gap: 15px;
}
.timezone-selector label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 14px;
color: #555;
}
.editor-pane {
display: flex;
gap: 20px;
flex: 1;
min-height: 0; /* Important for nested flex scrolling */
}
textarea {
flex: 1;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
resize: none;
font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Mono", "Droid Sans Mono", "Source Code Pro", monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre;
overflow-y: auto;
background-color: white;
}
textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.1);
}
#output {
background-color: #f8f9fa;
color: #333;
}

View file

@ -0,0 +1,103 @@
function processLine(line, operation, timezone) {
if (!line.trim() || line.trim().startsWith('#')) {
return line;
}
try {
let date;
const trimmedLine = line.trim();
switch (operation) {
case 'unix':
// Input is seconds
date = new Date(Number(trimmedLine) * 1000);
break;
case 'unix_micro':
// Input is milliseconds
date = new Date(Number(trimmedLine));
break;
case 'to_utc':
case 'from_utc':
// Input is ISO 8601
date = new Date(trimmedLine);
break;
}
if (isNaN(date.getTime())) {
return 'Invalid Date';
}
if (operation === 'to_utc') {
let dateToConvert = date;
if (timezone === 'utc' && !trimmedLine.toUpperCase().endsWith('Z') && !trimmedLine.match(/[+-]\d{2}:?\d{2}$/)) {
dateToConvert = new Date(trimmedLine + 'Z');
}
return dateToConvert.toISOString();
}
if (operation === 'from_utc') {
let dateFromUtc;
if (!trimmedLine.toUpperCase().endsWith('Z') && !trimmedLine.match(/[+-]\d{2}:?\d{2}$/)) {
dateFromUtc = new Date(trimmedLine + 'Z');
} else {
dateFromUtc = new Date(trimmedLine);
}
if (timezone === 'utc') {
return dateFromUtc.toISOString();
} else {
// Local
const offset = dateFromUtc.getTimezoneOffset();
const localDate = new Date(dateFromUtc.getTime() - (offset * 60 * 1000));
return localDate.toISOString().slice(0, -1);
}
}
// For Unix conversions:
if (timezone === 'utc') {
return date.toISOString();
} else {
// Local
const offset = date.getTimezoneOffset();
const localDate = new Date(date.getTime() - (offset * 60 * 1000));
return localDate.toISOString().slice(0, -1);
}
} catch (e) {
return 'Error';
}
}
// Tests
console.log("Running tests...");
// 1. Unix to UTC
const t1 = processLine('1672531200', 'unix', 'utc');
console.log(`1. Unix to UTC: ${t1} (Expected: 2023-01-01T00:00:00.000Z)`);
if (t1 !== '2023-01-01T00:00:00.000Z') console.error("FAIL");
// 2. Unix Micro to UTC
const t2 = processLine('1672531200000', 'unix_micro', 'utc');
console.log(`2. Unix Micro to UTC: ${t2} (Expected: 2023-01-01T00:00:00.000Z)`);
if (t2 !== '2023-01-01T00:00:00.000Z') console.error("FAIL");
// 3. To UTC (from Local input)
// Note: Node.js server might be in UTC or Local.
// If I run this on a machine, "Local" depends on system time.
// However, the logic for 'to_utc' with 'utc' timezone forces Z.
const t3 = processLine('2023-01-01T00:00:00', 'to_utc', 'utc');
console.log(`3. To UTC (input no offset, selected UTC): ${t3} (Expected: 2023-01-01T00:00:00.000Z)`);
if (t3 !== '2023-01-01T00:00:00.000Z') console.error("FAIL");
// 4. From UTC (to UTC)
const t4 = processLine('2023-01-01T00:00:00', 'from_utc', 'utc');
console.log(`4. From UTC (input no offset, selected UTC): ${t4} (Expected: 2023-01-01T00:00:00.000Z)`);
if (t4 !== '2023-01-01T00:00:00.000Z') console.error("FAIL");
// 5. Comment
const t5 = processLine('# comment', 'unix', 'utc');
console.log(`5. Comment: ${t5} (Expected: # comment)`);
if (t5 !== '# comment') console.error("FAIL");
console.log("Tests finished.");