From 67bd054f7b50b17ec7ebf3f30c554e400365da94 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sat, 21 Oct 2023 02:31:27 +0200 Subject: [PATCH] Metadata: Improve handling of local time values #3780 Signed-off-by: Michael Mayer --- frontend/package-lock.json | 128 ++++++++++---------- frontend/src/options/options.js | 2 - frontend/tests/unit/options/options_test.js | 4 +- internal/meta/json_exiftool.go | 24 ++-- pkg/txt/timezone.go | 100 +++++++-------- pkg/txt/timezone_test.go | 84 ++++++++----- 6 files changed, 179 insertions(+), 163 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 327c8c178..3de3f9484 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2710,19 +2710,19 @@ } }, "node_modules/@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.12.tgz", - "integrity": "sha512-NlGesA1usRNn6ctHCZ21M4/dKPgW9Nn1FypRdIKKgZOKzkVV4T1FlK5mBiLhHBCDmEbdQG0idrcXlbZfksJ+RA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.0", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -2743,9 +2743,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.0.tgz", - "integrity": "sha512-9S9QrXY2K0L4AGDcSgTi9vgiCcG8VcBv4Mp7/1hDPYoswIy6Z6KO5blYto82BT8M0MZNRWmCFLpCs3HlpYGGdw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==" }, "node_modules/@ioredis/commands": { "version": "1.2.0", @@ -2990,9 +2990,9 @@ } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "19.3.2", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.2.tgz", - "integrity": "sha512-C2JAk64XUz9v78+bpyTk1zvgjjnDsB8CCjNumyAYdWK2dvdDtILzh1AGBMdS/llX3KaHjGYxAE5wOwfdwq4Pog==", + "version": "19.3.3", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz", + "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", @@ -3216,37 +3216,42 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==" }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@vue/compiler-core": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.5.tgz", - "integrity": "sha512-S8Ma+eICI40Y4UotR+iKR729Bma+wERn/xLc+Jz203s5WIW1Sx3qoiONqXGg3Q4vBMa+QHDncULya19ZSJuhog==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.6.tgz", + "integrity": "sha512-2JNjemwaNwf+MkkatATVZi7oAH1Hx0B04DdPH3ZoZ8vKC1xZVP7nl4HIsk8XYd3r+/52sqqoz9TWzYc3yE9dqA==", "dependencies": { "@babel/parser": "^7.23.0", - "@vue/shared": "3.3.5", + "@vue/shared": "3.3.6", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.5.tgz", - "integrity": "sha512-dxt6QntN9T/NtnV6Pz+/nmcoo3ULnsYCnRpvEyY73wbk1tzzx7dnwngUN1cXkyGNu9c3UE7llhq/5T54lKwyhQ==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.6.tgz", + "integrity": "sha512-1MxXcJYMHiTPexjLAJUkNs/Tw2eDf2tY3a0rL+LfuWyiKN2s6jvSwywH3PWD8bKICjfebX3GWx2Os8jkRDq3Ng==", "dependencies": { - "@vue/compiler-core": "3.3.5", - "@vue/shared": "3.3.5" + "@vue/compiler-core": "3.3.6", + "@vue/shared": "3.3.6" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.5.tgz", - "integrity": "sha512-M6ys4iReSbrF4NTcMCnJiBioCpzXjfkfXwkdziknRyps+pG0DkwpDfQT7zQ0q91/rCR/Ejz64b5H6C4HBhX41w==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.6.tgz", + "integrity": "sha512-/Kms6du2h1VrXFreuZmlvQej8B1zenBqIohP0690IUBkJjsFvJxY0crcvVRJ0UhMgSR9dewB+khdR1DfbpArJA==", "dependencies": { "@babel/parser": "^7.23.0", - "@vue/compiler-core": "3.3.5", - "@vue/compiler-dom": "3.3.5", - "@vue/compiler-ssr": "3.3.5", - "@vue/reactivity-transform": "3.3.5", - "@vue/shared": "3.3.5", + "@vue/compiler-core": "3.3.6", + "@vue/compiler-dom": "3.3.6", + "@vue/compiler-ssr": "3.3.6", + "@vue/reactivity-transform": "3.3.6", + "@vue/shared": "3.3.6", "estree-walker": "^2.0.2", "magic-string": "^0.30.5", "postcss": "^8.4.31", @@ -3254,12 +3259,12 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.5.tgz", - "integrity": "sha512-v7p2XuEpOcgjd6c49NqOnq3UTJOv5Uo9tirOyGnEadwxTov2O1J3/TUt4SgAAnwA+9gcUyH5c3lIOFsBe+UIyw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.6.tgz", + "integrity": "sha512-QTIHAfDCHhjXlYGkUg5KH7YwYtdUM1vcFl/FxFDlD6d0nXAmnjizka3HITp8DGudzHndv2PjKVS44vqqy0vP4w==", "dependencies": { - "@vue/compiler-dom": "3.3.5", - "@vue/shared": "3.3.5" + "@vue/compiler-dom": "3.3.6", + "@vue/shared": "3.3.6" } }, "node_modules/@vue/component-compiler-utils": { @@ -3331,26 +3336,26 @@ "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" }, "node_modules/@vue/reactivity-transform": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.5.tgz", - "integrity": "sha512-OhpBD1H32pIapRzqy31hWwTFLf9STP+0uk5bVOQWXACTa2Rt/RPhvX4zixbPgMGo6iP+S+tFpZzUdcG8AASn8A==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.6.tgz", + "integrity": "sha512-RlJl4dHfeO7EuzU1iJOsrlqWyJfHTkJbvYz/IOJWqu8dlCNWtxWX377WI0VsbAgBizjwD+3ZjdnvSyyFW1YVng==", "dependencies": { "@babel/parser": "^7.23.0", - "@vue/compiler-core": "3.3.5", - "@vue/shared": "3.3.5", + "@vue/compiler-core": "3.3.6", + "@vue/shared": "3.3.6", "estree-walker": "^2.0.2", "magic-string": "^0.30.5" } }, "node_modules/@vue/shared": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.5.tgz", - "integrity": "sha512-oNJN1rCtkqm1cIxU1BuZVEVRWIp4DhaxXucEzzZ/iDKHP71ZxhkBPNK+URySiECH6aiOZzC60PS2bd6JFznvNA==" + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.6.tgz", + "integrity": "sha512-Xno5pEqg8SVhomD0kTSmfh30ZEmV/+jZtyh39q6QflrjdJCXah5lrnOLi9KB6a5k5aAHXMXjoMnxlzUkCNfWLQ==" }, "node_modules/@vvo/tzdb": { - "version": "6.108.0", - "resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.108.0.tgz", - "integrity": "sha512-/UI2yKYNlcPVsVajMNcLfcsZgD+TtmE9hsN+3JTrk8N4/Kwlr35SqMOZuSU7lwWG+PvWmWKs51f2SMM0JGWxww==" + "version": "6.109.0", + "resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.109.0.tgz", + "integrity": "sha512-HFE2m2YIiW0POGepiHAPYlqzv9YZxc96faxVH0UOen4Djvl+l3fSVeeTgQRCOCy+aKLtqALthVrVgt8BOlWkmg==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", @@ -5809,9 +5814,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.561", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.561.tgz", - "integrity": "sha512-eS5t4ulWOBfVHdq9SW2dxEaFarj1lPjvJ8PaYMOjY0DecBaj/t4ARziL2IPpDr4atyWwjLFGQ2vo/VCgQFezVQ==" + "version": "1.4.563", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.563.tgz", + "integrity": "sha512-dg5gj5qOgfZNkPNeyKBZQAQitIQ/xwfIDmEQJHCbXaD9ebTZxwJXUsDYcBlAvZGZLi+/354l35J1wkmP6CqYaw==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -6022,17 +6027,18 @@ } }, "node_modules/eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -7460,19 +7466,19 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/flow-parser": { - "version": "0.219.2", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.219.2.tgz", - "integrity": "sha512-OqzmNECXX85x/5L/OP9TfHErdDoSUoKR4y1sTTy/A5K2arwl7s5EmX0XTkkcJPlCAHYkElWj5Se+ZwNN/6ry2Q==", + "version": "0.219.3", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.219.3.tgz", + "integrity": "sha512-dyPC0+TwAcBMQ1IZhSpj91mxZ31AI9FJ3q/ZMt8kdKaITnDCGmyUyWOwUfAKBVLrUTkdaTfpla0muhwOGY+dXw==", "engines": { "node": ">=0.4.0" } }, "node_modules/flow-remove-types": { - "version": "2.219.2", - "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.219.2.tgz", - "integrity": "sha512-g0BFqtf882YOntBvMSXXz7qTEsIKuLBefzk0mLy3PkRDDty1jYmxAorDg9xY7ydWNyONohNaeNVg4x33wGpWlw==", + "version": "2.219.3", + "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.219.3.tgz", + "integrity": "sha512-xYAJIcShkcYALDbMbGGDqOgZTEdH56QbF6M6pOqU1Nww9m1U7y1PJpBXkQIlOolvqZyaEy/gDr0gNweOOspJyg==", "dependencies": { - "flow-parser": "^0.219.2", + "flow-parser": "^0.219.3", "pirates": "^3.0.2", "vlq": "^0.2.1" }, diff --git a/frontend/src/options/options.js b/frontend/src/options/options.js index 42b5b39be..5a90fac6a 100644 --- a/frontend/src/options/options.js +++ b/frontend/src/options/options.js @@ -12,8 +12,6 @@ import { } from "model/photo"; export const UtcOffsets = [ - { ID: "UTC-14", Name: "UTC-14:00" }, - { ID: "UTC-13", Name: "UTC-13:00" }, { ID: "UTC-12", Name: "UTC-12:00" }, { ID: "UTC-11", Name: "UTC-11:00" }, { ID: "UTC-10", Name: "UTC-10:00" }, diff --git a/frontend/tests/unit/options/options_test.js b/frontend/tests/unit/options/options_test.js index 760805a6f..054537005 100644 --- a/frontend/tests/unit/options/options_test.js +++ b/frontend/tests/unit/options/options_test.js @@ -9,8 +9,8 @@ describe("options/options", () => { const timezones = options.TimeZones(); assert.equal(timezones[0].ID, ""); assert.equal(timezones[0].Name, "Local Time"); - assert.equal(timezones[1].ID, "UTC-14"); - assert.equal(timezones[1].Name, "UTC-14:00"); + assert.equal(timezones[1].ID, "UTC-12"); + assert.equal(timezones[1].Name, "UTC-12:00"); }); it("should get days", () => { diff --git a/internal/meta/json_exiftool.go b/internal/meta/json_exiftool.go index ad52ff923..bec382579 100644 --- a/internal/meta/json_exiftool.go +++ b/internal/meta/json_exiftool.go @@ -243,49 +243,49 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) { // Set time zone and calculate UTC time. if data.Lat != 0 && data.Lng != 0 { - zones, err := tz.GetZone(tz.Point{ + zones, zoneErr := tz.GetZone(tz.Point{ Lat: float64(data.Lat), Lon: float64(data.Lng), }) - if err == nil && len(zones) > 0 { + if zoneErr == nil && len(zones) > 0 { data.TimeZone = zones[0] } if loc := txt.TimeZone(data.TimeZone); loc == nil { log.Warnf("metadata: %s has invalid time zone %s (exiftool)", logName) } else if !data.TakenAtLocal.IsZero() { - if tl, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), loc); err == nil { + if tl, parseErr := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), loc); parseErr == nil { if localUtc, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), time.UTC); err == nil { data.TakenAtLocal = localUtc } data.TakenAt = tl.Truncate(time.Second).UTC() } else { - log.Errorf("metadata: %s (exiftool)", err.Error()) // this should never happen + log.Errorf("metadata: %s (exiftool)", parseErr.Error()) // this should never happen } } else if !data.TakenAt.IsZero() { - if localUtc, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAt.In(loc).Format("2006:01:02 15:04:05"), time.UTC); err == nil { + if localUtc, parseErr := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAt.In(loc).Format("2006:01:02 15:04:05"), time.UTC); parseErr == nil { data.TakenAtLocal = localUtc data.TakenAt = data.TakenAt.UTC() } else { - log.Errorf("metadata: %s (exiftool)", err.Error()) // this should never happen + log.Errorf("metadata: %s (exiftool)", parseErr.Error()) // this should never happen } } } else if hasTimeOffset { - if localUtc, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), time.UTC); err == nil { + if localUtc, parseErr := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), time.UTC); parseErr == nil { data.TakenAtLocal = localUtc.Truncate(time.Second).UTC() } data.TakenAt = data.TakenAt.Truncate(time.Second).UTC() } - // Default to UTC offset time zone? - if data.TimeZone != "" && data.TimeZone != "UTC" || data.TakenAtLocal.IsZero() || data.TakenAt.IsZero() { + // Set UTC offset as time zone? + if data.TimeZone != "" && data.TimeZone != "UTC" || data.TakenAt.IsZero() { // Don't change existing time zone. - } else if z := txt.UtcOffset(data.TakenAtLocal, data.TakenAt, data.TimeOffset); z != "" { - data.TimeZone = z - log.Infof("metadata: %s has time offset %s (exiftool)", logName, clean.Log(data.TimeZone)) + } else if utcOffset := txt.UtcOffset(data.TakenAtLocal, data.TakenAt, data.TimeOffset); utcOffset != "" { + data.TimeZone = utcOffset + log.Infof("metadata: %s has time offset %s (exiftool)", logName, clean.Log(utcOffset)) } else if data.TimeOffset != "" { log.Infof("metadata: %s has invalid time offset %s (exiftool)", logName, clean.Log(data.TimeOffset)) } diff --git a/pkg/txt/timezone.go b/pkg/txt/timezone.go index 2d218f61d..6f4476ffe 100644 --- a/pkg/txt/timezone.go +++ b/pkg/txt/timezone.go @@ -9,30 +9,19 @@ import ( // TimeZone returns a time zone for the given UTC offset string. func TimeZone(offset string) *time.Location { - if IsUtcOffset(offset) { - sec := TimeOffset(offset) - if sec == 0 { - return time.UTC + if offset == "" { + // Local time. + } else if offset == "UTC" || offset == "Z" { + return time.UTC + } else if seconds, err := TimeOffset(offset); err == nil { + if h := seconds / 3600; h > 0 || h < 0 { + return time.FixedZone(fmt.Sprintf("UTC%+d", h), seconds) } - return time.FixedZone(fmt.Sprintf("UTC%+d", sec/3600), sec) - } else if location, err := time.LoadLocation(offset); err == nil { - return location + } else if zone, zoneErr := time.LoadLocation(offset); zoneErr == nil { + return zone } - return nil -} - -// IsUtcOffset checks if the string is a valid UTC time offset. -func IsUtcOffset(s string) bool { - if l := len(s); l < 3 || l > 6 { - return false - } else if s == "UTC" { - return true - } else if !strings.HasPrefix(s, "UTC") { - return false - } - - return TimeOffset(s) != 0 + return time.FixedZone("", 0) } // NormalizeUtcOffset returns a normalized UTC time offset string. @@ -68,7 +57,7 @@ func NormalizeUtcOffset(s string) string { return "UTC-2" case "-1", "-01", "-01:00", "UTC-1", "UTC-01:00": return "UTC-1" - case "+0", "+00", "00:00", "+00:00", "-00:00", "Z", "Z00:00", "UTC", "UTC+0", "UTC-0", "UTC+00:00", "UTC-00:00": + case "Z", "UTC", "UTC+0", "UTC-0", "UTC+00:00", "UTC-00:00": return time.UTC.String() case "01:00", "+1", "+01", "+01:00", "UTC+1", "UTC+01:00": return "UTC+1" @@ -117,66 +106,69 @@ func UtcOffset(local, utc time.Time, offset string) string { } // Check if time difference is within expected range (hours). - if d < -12 || d > 12 { + if h := int(d); h == 0 || h < -12 || h > 12 { return "" - } else if d == 0 { - return time.UTC.String() + } else { + return fmt.Sprintf("UTC%+d", h) } - - return fmt.Sprintf("UTC%+d", int(d)) } -func TimeOffset(s string) (seconds int) { - switch s { +// TimeOffset returns the UTC time offset in seconds or an error if it is invalid. +func TimeOffset(utcOffset string) (seconds int, err error) { + switch utcOffset { case "-12", "-12:00", "UTC-12", "UTC-12:00": - return -12 * 3600 + seconds = -12 * 3600 case "-11", "-11:00", "UTC-11", "UTC-11:00": - return -11 * 3600 + seconds = -11 * 3600 case "-10", "-10:00", "UTC-10", "UTC-10:00": - return -10 * 3600 + seconds = -10 * 3600 case "-9", "-09", "-09:00", "UTC-9", "UTC-09:00": - return -9 * 3600 + seconds = -9 * 3600 case "-8", "-08", "-08:00", "UTC-8", "UTC-08:00": - return -8 * 3600 + seconds = -8 * 3600 case "-7", "-07", "-07:00", "UTC-7", "UTC-07:00": - return -7 * 3600 + seconds = -7 * 3600 case "-6", "-06", "-06:00", "UTC-6", "UTC-06:00": - return -6 * 3600 + seconds = -6 * 3600 case "-5", "-05", "-05:00", "UTC-5", "UTC-05:00": - return -5 * 3600 + seconds = -5 * 3600 case "-4", "-04", "-04:00", "UTC-4", "UTC-04:00": - return -4 * 3600 + seconds = -4 * 3600 case "-3", "-03", "-03:00", "UTC-3", "UTC-03:00": - return -3 * 3600 + seconds = -3 * 3600 case "-2", "-02", "-02:00", "UTC-2", "UTC-02:00": - return -2 * 3600 + seconds = -2 * 3600 case "-1", "-01", "-01:00", "UTC-1", "UTC-01:00": - return -1 * 3600 + seconds = -1 * 3600 case "01:00", "+1", "+01", "+01:00", "UTC+1", "UTC+01:00": - return 1 * 3600 + seconds = 1 * 3600 case "02:00", "+2", "+02", "+02:00", "UTC+2", "UTC+02:00": - return 2 * 3600 + seconds = 2 * 3600 case "03:00", "+3", "+03", "+03:00", "UTC+3", "UTC+03:00": - return 3 * 3600 + seconds = 3 * 3600 case "04:00", "+4", "+04", "+04:00", "UTC+4", "UTC+04:00": - return 4 * 3600 + seconds = 4 * 3600 case "05:00", "+5", "+05", "+05:00", "UTC+5", "UTC+05:00": - return 5 * 3600 + seconds = 5 * 3600 case "06:00", "+6", "+06", "+06:00", "UTC+6", "UTC+06:00": - return 6 * 3600 + seconds = 6 * 3600 case "07:00", "+7", "+07", "+07:00", "UTC+7", "UTC+07:00": - return 7 * 3600 + seconds = 7 * 3600 case "08:00", "+8", "+08", "+08:00", "UTC+8", "UTC+08:00": - return 8 * 3600 + seconds = 8 * 3600 case "09:00", "+9", "+09", "+09:00", "UTC+9", "UTC+09:00": - return 9 * 3600 + seconds = 9 * 3600 case "10:00", "+10", "+10:00", "UTC+10", "UTC+10:00": - return 10 * 3600 + seconds = 10 * 3600 case "11:00", "+11", "+11:00", "UTC+11", "UTC+11:00": - return 11 * 3600 + seconds = 11 * 3600 case "12:00", "+12", "+12:00", "UTC+12", "UTC+12:00": - return 12 * 3600 + seconds = 12 * 3600 + case "Z", "UTC", "UTC+0", "UTC-0", "UTC+00:00", "UTC-00:00": + seconds = 0 default: - return 0 + return 0, fmt.Errorf("invalid UTC offset") } + + return seconds, nil } diff --git a/pkg/txt/timezone_test.go b/pkg/txt/timezone_test.go index 9f35126df..9021424b6 100644 --- a/pkg/txt/timezone_test.go +++ b/pkg/txt/timezone_test.go @@ -10,6 +10,14 @@ import ( func TestTimeZone(t *testing.T) { t.Run("UTC", func(t *testing.T) { assert.Equal(t, time.UTC.String(), TimeZone(time.UTC.String()).String()) + assert.Equal(t, time.UTC.String(), TimeZone("Z").String()) + assert.Equal(t, time.UTC.String(), TimeZone("UTC").String()) + }) + t.Run("LocalTime", func(t *testing.T) { + assert.Equal(t, "", TimeZone("").String()) + assert.Equal(t, "", TimeZone("0").String()) + assert.Equal(t, "", TimeZone("UTC+0").String()) + assert.Equal(t, "", TimeZone("UTC+00:00").String()) }) t.Run("UTC+2", func(t *testing.T) { local, err := time.Parse("2006-01-02 15:04:05 Z07:00", "2023-10-02 13:20:17 +00:00") @@ -34,24 +42,6 @@ func TestTimeZone(t *testing.T) { }) } -func TestIsUtcOffset(t *testing.T) { - t.Run("Valid", func(t *testing.T) { - assert.Equal(t, true, IsUtcOffset("UTC-2")) - assert.Equal(t, true, IsUtcOffset("UTC")) - assert.Equal(t, true, IsUtcOffset("UTC+1")) - assert.Equal(t, true, IsUtcOffset("UTC+2")) - assert.Equal(t, true, IsUtcOffset("UTC+12")) - }) - t.Run("Invalid", func(t *testing.T) { - assert.Equal(t, false, IsUtcOffset("UTC-15")) - assert.Equal(t, false, IsUtcOffset("UTC-14")) - assert.Equal(t, false, IsUtcOffset("UTC--2")) - assert.Equal(t, false, IsUtcOffset("UTC1")) - assert.Equal(t, false, IsUtcOffset("UTC13")) - assert.Equal(t, false, IsUtcOffset("UTC+13")) - }) -} - func TestNormalizeUtcOffset(t *testing.T) { t.Run("Valid", func(t *testing.T) { assert.Equal(t, "UTC-2", NormalizeUtcOffset("UTC-2")) @@ -112,8 +102,8 @@ func TestUtcOffset(t *testing.T) { t.Fatal(err) } - assert.Equal(t, "UTC", UtcOffset(local, utc, "00:00")) - assert.Equal(t, "UTC", UtcOffset(local, utc, "+00:00")) + assert.Equal(t, "", UtcOffset(local, utc, "00:00")) + assert.Equal(t, "", UtcOffset(local, utc, "+00:00")) assert.Equal(t, "UTC", UtcOffset(local, utc, "Z")) }) t.Run("UTC+2", func(t *testing.T) { @@ -216,19 +206,49 @@ func TestUtcOffset(t *testing.T) { func TestTimeOffset(t *testing.T) { t.Run("Valid", func(t *testing.T) { - assert.Equal(t, -2*3600, TimeOffset("UTC-2")) - assert.Equal(t, 0, TimeOffset("UTC")) - assert.Equal(t, 3600, TimeOffset("UTC+1")) - assert.Equal(t, 2*3600, TimeOffset("UTC+2")) - assert.Equal(t, 12*3600, TimeOffset("UTC+12")) + sec, err := TimeOffset("UTC-2") + assert.Equal(t, -2*3600, sec) + assert.NoError(t, err) + + sec, err = TimeOffset("UTC") + assert.Equal(t, 0, sec) + assert.NoError(t, err) + + sec, err = TimeOffset("UTC+1") + assert.Equal(t, 3600, sec) + assert.NoError(t, err) + + sec, err = TimeOffset("UTC+2") + assert.Equal(t, 2*3600, sec) + assert.NoError(t, err) + + sec, err = TimeOffset("UTC+12") + assert.Equal(t, 12*3600, sec) + assert.NoError(t, err) }) t.Run("Invalid", func(t *testing.T) { - assert.Equal(t, 0, TimeOffset("UTC-15")) - assert.Equal(t, 0, TimeOffset("UTC-14")) - assert.Equal(t, 0, TimeOffset("UTC--2")) - assert.Equal(t, 0, TimeOffset("UTC0")) - assert.Equal(t, 0, TimeOffset("UTC1")) - assert.Equal(t, 0, TimeOffset("UTC13")) - assert.Equal(t, 0, TimeOffset("UTC+13")) + sec, err := TimeOffset("UTC-15") + assert.Equal(t, 0, sec) + assert.Error(t, err) + + sec, err = TimeOffset("UTC--2") + assert.Equal(t, 0, sec) + assert.Error(t, err) + + sec, err = TimeOffset("UTC0") + assert.Equal(t, 0, sec) + assert.Error(t, err) + + sec, err = TimeOffset("UTC1") + assert.Equal(t, 0, sec) + assert.Error(t, err) + + sec, err = TimeOffset("UTC13") + assert.Equal(t, 0, sec) + assert.Error(t, err) + + sec, err = TimeOffset("UTC+13") + assert.Equal(t, 0, sec) + assert.Error(t, err) }) }