Vibe coded "timestamps"
This commit is contained in:
parent
35270f72ca
commit
7bf586757a
1
Makefile
1
Makefile
|
|
@ -13,6 +13,7 @@ build.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/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/.
|
||||
|
||||
.Phony: build.site
|
||||
|
|
|
|||
200
cmds/timestamps/main.go
Normal file
200
cmds/timestamps/main.go
Normal 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")
|
||||
}
|
||||
}
|
||||
38
site/timestamps/index.html
Normal file
38
site/timestamps/index.html
Normal 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
8
site/timestamps/main.js
Normal 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
91
site/timestamps/style.css
Normal 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;
|
||||
}
|
||||
103
site/timestamps/test_logic.js
Normal file
103
site/timestamps/test_logic.js
Normal 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.");
|
||||
Loading…
Reference in a new issue