diff --git a/clock.go b/clock.go index a099fd0..ff60f8d 100644 --- a/clock.go +++ b/clock.go @@ -38,22 +38,18 @@ func (c *Clock) AddDays(n int) { c.isRealTime = false } -// AddDays adds n days to the current date and clears the minutes +// AddDays adds n hours to the current date-time, keeping the minutes func (c *Clock) AddHours(n int) { - c.t = time.Date( - c.t.Year(), - c.t.Month(), - c.t.Day(), - c.t.Hour(), - 0, // Minutes set to 0 - 0, // Seconds set to 0 - 0, // Nanoseconds set to 0 - c.t.Location(), - ) c.t = c.t.Add(time.Hour * time.Duration(n)) c.isRealTime = false } +// AddMinutes adds n minutes to the current date-time, keeping the seconds +func (c *Clock) AddMinutes(n int) { + c.t = c.t.Add(time.Minute * time.Duration(n)) + c.isRealTime = false +} + // Get the wrapped time.Time struct func (c *Clock) Time() time.Time { return c.t diff --git a/config.go b/config.go index f68205a..d6d752f 100644 --- a/config.go +++ b/config.go @@ -20,16 +20,26 @@ import ( "fmt" "slices" "strings" + "time" ) // Keymaps represents the key mappings in the TOML file type Keymaps struct { + PrevMinute []string + NextMinute []string + ZeroMinute []string PrevHour []string NextHour []string PrevDay []string NextDay []string PrevWeek []string NextWeek []string + PrevLine []string + NextLine []string + PrevFStyle []string + NextFStyle []string + PrevZStyle []string + NextZStyle []string ToggleDate []string OpenWeb []string Now []string @@ -45,12 +55,21 @@ type Config struct { // Function to provide default values for the Config struct var DefaultKeymaps = Keymaps{ + PrevMinute: []string{"-"}, + NextMinute: []string{"+"}, + ZeroMinute: []string{"0"}, PrevHour: []string{"h", "left"}, NextHour: []string{"l", "right"}, PrevDay: []string{"H", "shift+left", "pgup", "shift+up", "ctrl+u"}, NextDay: []string{"L", "shift+right", "pgdown", "shift+down", "ctrl+d"}, PrevWeek: []string{"p", "ctrl+left", "shift+pgup", "ctrl+b"}, NextWeek: []string{"n", "ctrl+right", "shift+pgdown", "ctrl+f"}, + PrevLine: []string{"k", "up"}, + NextLine: []string{"j", "down"}, + PrevFStyle: []string{"F"}, + NextFStyle: []string{"f"}, + PrevZStyle: []string{"Z"}, + NextZStyle: []string{"z"}, ToggleDate: []string{"d"}, OpenWeb: []string{"o"}, Now: []string{"t"}, @@ -67,14 +86,16 @@ func LoadDefaultConfig(tzConfigs []string) (*Config, error) { } func LoadConfig(tomlFile string, tzConfigs []string) (*Config, error) { + now := time.Now() + // Apply config file first - fileConfig, fileError := LoadConfigFile(tomlFile) + fileConfig, fileError := LoadConfigFile(tomlFile, now) if fileError != nil { return nil, fmt.Errorf("File error: %w", fileError) } // Override with env var config - envConfig, envErr := LoadConfigEnv(tzConfigs) + envConfig, envErr := LoadConfigEnv(tzConfigs, now) if envErr != nil { return nil, fmt.Errorf("Env error: %w", envErr) } @@ -101,6 +122,18 @@ func LoadConfig(tomlFile string, tzConfigs []string) (*Config, error) { logger.Printf("Merged zones: %s", mergedConfig.Zones) // Merge Keymaps + if len(fileConfig.Keymaps.PrevMinute) > 0 { + mergedConfig.Keymaps.PrevMinute = fileConfig.Keymaps.PrevMinute + } + + if len(fileConfig.Keymaps.NextMinute) > 0 { + mergedConfig.Keymaps.NextMinute = fileConfig.Keymaps.NextMinute + } + + if len(fileConfig.Keymaps.ZeroMinute) > 0 { + mergedConfig.Keymaps.ZeroMinute = fileConfig.Keymaps.ZeroMinute + } + if len(fileConfig.Keymaps.PrevHour) > 0 { mergedConfig.Keymaps.PrevHour = fileConfig.Keymaps.PrevHour } @@ -125,6 +158,30 @@ func LoadConfig(tomlFile string, tzConfigs []string) (*Config, error) { mergedConfig.Keymaps.NextWeek = fileConfig.Keymaps.NextWeek } + if len(fileConfig.Keymaps.PrevLine) > 0 { + mergedConfig.Keymaps.PrevLine = fileConfig.Keymaps.PrevLine + } + + if len(fileConfig.Keymaps.NextLine) > 0 { + mergedConfig.Keymaps.NextLine = fileConfig.Keymaps.NextLine + } + + if len(fileConfig.Keymaps.PrevFStyle) > 0 { + mergedConfig.Keymaps.PrevFStyle = fileConfig.Keymaps.PrevFStyle + } + + if len(fileConfig.Keymaps.NextFStyle) > 0 { + mergedConfig.Keymaps.NextFStyle = fileConfig.Keymaps.NextFStyle + } + + if len(fileConfig.Keymaps.PrevZStyle) > 0 { + mergedConfig.Keymaps.PrevZStyle = fileConfig.Keymaps.PrevZStyle + } + + if len(fileConfig.Keymaps.NextZStyle) > 0 { + mergedConfig.Keymaps.NextZStyle = fileConfig.Keymaps.NextZStyle + } + if len(fileConfig.Keymaps.ToggleDate) > 0 { mergedConfig.Keymaps.ToggleDate = fileConfig.Keymaps.ToggleDate } @@ -137,17 +194,30 @@ func LoadConfig(tomlFile string, tzConfigs []string) (*Config, error) { mergedConfig.Keymaps.Now = fileConfig.Keymaps.Now } + if len(fileConfig.Keymaps.Help) > 0 { + mergedConfig.Keymaps.Help = fileConfig.Keymaps.Help + } + if len(fileConfig.Keymaps.Quit) > 0 { mergedConfig.Keymaps.Quit = fileConfig.Keymaps.Quit } allKeymaps := [][]string { + mergedConfig.Keymaps.PrevMinute, + mergedConfig.Keymaps.NextMinute, + mergedConfig.Keymaps.ZeroMinute, mergedConfig.Keymaps.PrevHour, mergedConfig.Keymaps.NextHour, mergedConfig.Keymaps.PrevDay, mergedConfig.Keymaps.NextDay, mergedConfig.Keymaps.PrevWeek, mergedConfig.Keymaps.NextWeek, + mergedConfig.Keymaps.PrevLine, + mergedConfig.Keymaps.NextLine, + mergedConfig.Keymaps.PrevFStyle, + mergedConfig.Keymaps.NextFStyle, + mergedConfig.Keymaps.PrevZStyle, + mergedConfig.Keymaps.NextZStyle, mergedConfig.Keymaps.ToggleDate, mergedConfig.Keymaps.OpenWeb, mergedConfig.Keymaps.Now, diff --git a/config_env.go b/config_env.go index 13f336f..c26b6d8 100644 --- a/config_env.go +++ b/config_env.go @@ -24,7 +24,7 @@ import ( ) // LoadConfigEnv from environment -func LoadConfigEnv(tzConfigs []string) (*Config, error) { +func LoadConfigEnv(tzConfigs []string, now time.Time) (*Config, error) { conf := Config{} if len(tzConfigs) == 0 { @@ -41,7 +41,7 @@ func LoadConfigEnv(tzConfigs []string) (*Config, error) { // Add zones from TZ_LIST for i, zoneConf := range tzConfigs { - zone, err := ReadZoneFromString(time.Now(), zoneConf) + zone, err := ReadZoneFromString(now, zoneConf) if err != nil { return nil, err } @@ -68,10 +68,9 @@ func ReadZoneFromString(now time.Time, zoneConf string) (*Zone, error) { if name == "" { name = loc.String() } - then := now.In(loc) - shortName, _ := then.Zone() return &Zone{ + Loc: loc, DbName: loc.String(), - Name: fmt.Sprintf("(%s) %s", shortName, name), + Name: name, }, nil } diff --git a/config_file.go b/config_file.go index 4908436..b45cd98 100644 --- a/config_file.go +++ b/config_file.go @@ -24,12 +24,21 @@ type ConfigFileZone struct { // Keymaps represents the key mappings in the TOML file type ConfigFileKeymaps struct { + PrevMinute []string `toml:"prev_minute"` + NextMinute []string `toml:"next_minute"` + ZeroMinute []string `toml:"zero_minute"` PrevHour []string `toml:"prev_hour"` NextHour []string `toml:"next_hour"` PrevDay []string `toml:"prev_day"` NextDay []string `toml:"next_day"` PrevWeek []string `toml:"prev_week"` NextWeek []string `toml:"next_week"` + PrevLine []string `toml:"prev_line_select"` + NextLine []string `toml:"next_line_select"` + PrevFStyle []string `toml:"prev_format_style"` + NextFStyle []string `toml:"next_format_style"` + PrevZStyle []string `toml:"prev_zone_style"` + NextZStyle []string `toml:"next_zone_style"` ToggleDate []string `toml:"toggle_date"` OpenWeb []string `toml:"open_web"` Now []string `toml:"now"` @@ -48,11 +57,10 @@ func ReadZonesFromFile(now time.Time, zoneConf ConfigFileZone) (*Zone, error) { if name == "" { name = loc.String() } - then := now.In(loc) - shortName, _ := then.Zone() return &Zone{ + Loc: loc, DbName: loc.String(), - Name: fmt.Sprintf("(%s) %s", shortName, name), + Name: name, }, nil } @@ -67,7 +75,7 @@ func DefaultConfigFile() (*string, error) { return &configFilePath, nil } -func LoadConfigFile(configFilePath string) (*Config, error) { +func LoadConfigFile(configFilePath string, now time.Time) (*Config, error) { conf := Config{} configFile, err := os.ReadFile(configFilePath) @@ -85,7 +93,7 @@ func LoadConfigFile(configFilePath string) (*Config, error) { // Add zones from config file zones := make([]*Zone, len(config.Zones)) for i, zoneConf := range config.Zones { - zone, err := ReadZonesFromFile(time.Now(), zoneConf) + zone, err := ReadZonesFromFile(now, zoneConf) if err != nil { return nil, err } diff --git a/config_file_test.go b/config_file_test.go index 13f4011..caf3aaa 100644 --- a/config_file_test.go +++ b/config_file_test.go @@ -18,6 +18,7 @@ package main import ( "testing" + "time" ) func TestDefaultConfigFile(t *testing.T) { @@ -28,8 +29,9 @@ func TestDefaultConfigFile(t *testing.T) { } func TestExampleConfigFile(t *testing.T) { + now := time.Now() tomlPath := "./example-conf.toml" - config, err := LoadConfigFile(tomlPath) + config, err := LoadConfigFile(tomlPath, now) if err != nil { t.Fatalf("Could not read test config from %s: %v", tomlPath, err) } diff --git a/config_test.go b/config_test.go index f5da9d8..f0530c4 100644 --- a/config_test.go +++ b/config_test.go @@ -52,6 +52,42 @@ func TestLoadConfig(t *testing.T) { } } +func TestLoadConfigParser(t *testing.T) { + tests := []struct { + args []string + envArg string + expected string + }{ + {nil, "", "Local;UTC"}, + {[]string{"GMT"}, "UTC", "Local;GMT"}, + {[]string{"", "GMT", ""}, "UTC", "Local;UTC;GMT;UTC"}, + {nil, "GMT", "Local;GMT"}, + {nil, ";GMT;", "Local;UTC;GMT;UTC"}, + {nil, "Unknown", ""}, + } + + oldEnv := os.Getenv("TZ_LIST") + for _, test := range tests { + os.Setenv("TZ_LIST", test.envArg) + config, err := LoadDefaultConfig(test.args) + if err != nil { + if test.expected != "" { + t.Error(err) + } + } else { + names := make([]string, len(config.Zones)) + for i, z := range config.Zones { + names[i] = z.Name + } + observed := strings.Join(names, ";") + if observed != test.expected { + t.Errorf("Expected '%v' to be: '%v'", observed, test.expected) + } + } + } + os.Setenv("TZ_LIST", oldEnv) +} + func TestLoadDefaultConfig(t *testing.T) { _, err := LoadDefaultConfig(nil) if err != nil { diff --git a/example-conf.toml b/example-conf.toml index 155dfbd..d424524 100644 --- a/example-conf.toml +++ b/example-conf.toml @@ -15,13 +15,23 @@ id = "UTC" name = "UTC" [keymaps] +prev_minute = ["-"] +next_minute = ["+"] +zero_minute = ["0"] prev_hour = ["h", "left"] next_hour = ["l", "right"] prev_day = ["k", "up"] next_day = ["j", "down"] prev_week = ["p", "<"] next_week = ["n", ">"] +prev_line_select = [","] +next_line_select = ["."] +next_format_style = ["f"] +prev_format_style = ["F"] +next_zone_style = ["z"] +prev_zone_style = ["Z"] toggle_date = ["d"] open_web = ["o", "x"] now = ["t"] +help = ["f1"] quit = ["q", "esc", "ctrl+c"] diff --git a/main.go b/main.go index 00eb8c1..f58b963 100644 --- a/main.go +++ b/main.go @@ -76,11 +76,14 @@ type model struct { zones []*Zone keymaps Keymaps clock Clock + highlighted int // 0 == none, else row number indexed from 1 showDates bool interactive bool isMilitary bool watch bool showHelp bool + formatStyle FormatStyle + zoneStyle ZoneStyle } func (m model) Init() tea.Cmd { @@ -107,6 +110,24 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case match(key, m.keymaps.Quit): return m, tea.Quit + case match(key, m.keymaps.PrevMinute): + m.clock.AddMinutes(-1) + + case match(key, m.keymaps.NextMinute): + m.clock.AddMinutes(1) + + case match(key, m.keymaps.ZeroMinute): + m.clock = *NewClockTime(time.Date( + m.clock.t.Year(), + m.clock.t.Month(), + m.clock.t.Day(), + m.clock.t.Hour(), + 0, + 0, + 0, + m.clock.t.Location(), + )) + case match(key, m.keymaps.PrevHour): m.clock.AddHours(-1) @@ -125,6 +146,26 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case match(key, m.keymaps.NextWeek): m.clock.AddDays(7) + case match(key, m.keymaps.PrevLine): + modulo := len(m.zones) + 1 + m.highlighted = (m.highlighted - 1 + modulo) % modulo + + case match(key, m.keymaps.NextLine): + modulo := len(m.zones) + 1 + m.highlighted = (m.highlighted + 1) % modulo + + case match(key, m.keymaps.NextFStyle): + m.formatStyle = m.formatStyle.next() + + case match(key, m.keymaps.PrevFStyle): + m.formatStyle = m.formatStyle.previous() + + case match(key, m.keymaps.PrevZStyle): + m.zoneStyle = m.zoneStyle.previous() + + case match(key, m.keymaps.NextZStyle): + m.zoneStyle = m.zoneStyle.next() + case match(key, m.keymaps.OpenWeb): openInTimeAndDateDotCom(m.clock.Time()) @@ -188,6 +229,7 @@ func main() { isMilitary: *military, watch: *watch, showHelp: false, + zoneStyle: AbbreviationZoneStyle, } if *when != 0 { diff --git a/main_test.go b/main_test.go index bfe8ad9..344f1b9 100644 --- a/main_test.go +++ b/main_test.go @@ -55,9 +55,9 @@ func getTimestampWithHour(hour int) time.Time { time.Now().Month(), time.Now().Day(), hour, - 0, // Minutes set to 0 - 0, // Seconds set to 0 - 0, // Nanoseconds set to 0 + 43, // Minutes + 59, // Seconds + 127, // Nanoseconds time.Now().Location(), ) } @@ -102,9 +102,11 @@ func TestUpdateIncHour(t *testing.T) { clock: *NewClockTime(getTimestampWithHour(test.startHour)), } - db := m.clock.Time().Day() + tb := m.clock.Time() + db := tb.Day() _, cmd := m.Update(msg) - da := m.clock.Time().Day() + ta := m.clock.Time() + da := ta.Day() if cmd != nil { t.Errorf("Expected nil Cmd, but got %v", cmd) @@ -117,6 +119,9 @@ func TestUpdateIncHour(t *testing.T) { if test.changesClockDateBy != 0 && da == db { t.Errorf("Expected date change of %d day, but got %d", test.changesClockDateBy, da-db) } + if ta.Minute() != tb.Minute() { + t.Errorf("Unexpected change of minute from '%s' to '%s'", tb, ta) + } } } diff --git a/testdata/main/test-military-time.txt b/testdata/main/test-military-time.txt index 3ef6377..5fbb89f 100644 --- a/testdata/main/test-military-time.txt +++ b/testdata/main/test-military-time.txt @@ -7,6 +7,6 @@ However for clarity, we include a colon delimiter between hours and minutes anyw What time is it? - πŸ•› UTC 00:01, Sun Nov 05, 2017 + πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 πŸ“† Sun 05 diff --git a/testdata/view/test-dst-end-days.txt b/testdata/view/test-dst-end-days.txt new file mode 100644 index 0000000..03ebb33 --- /dev/null +++ b/testdata/view/test-dst-end-days.txt @@ -0,0 +1,278 @@ +When daylight saving ends, clocks go backward 1 hour, so the date has 25 hours not 24 hours. +Using UTC as the local timezone (simple hours from 0-23 with no daylight saving changes), +render before/after the end of October daylight saving in Europe (Sunday) and Israel (Friday). +This checks the following requirements: +- Alignment of hours between different timezones. +- Duplicated hours when DST ends (e.g. morning hour 2 should be doubled in that zone, only). +- Abbreviated timezone names are updated before/after DST transitions: + - Central European Summer Time (CEST) versus CET (Central European Time) + - Israel Daylight Time (IDT) versus IST (Israel Standard Time) +- Timezones with same abbreviations but different offsets are handled correctly: + - IST: Israel Standard Time versus Indian Standard Time + - CDT: Central Daylight Time versus Cuba Daylight Time +- Timezones with positive and negative offsets. +- Timezones with different dates (before/after the International Date Line) +- Timezones with fractional offsets. +- Timezones with short and long names. +- Date transitions. +- Column location of showDates should shift by one hour between days before/after DST. + +-- Europe DST end (2024-10-27T01:00:00Z = 1729990800) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Sun Oct 27, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 27 + πŸ•‘ (CET) Europe/Paris 02:00, Sun Oct 27, 2024 + 2 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + β‰ DST πŸ“† Mon 28 + πŸ•’ (IST) Israel 03:00, Sun Oct 27, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 28 + πŸ•• (IST) Asia/Calcutta 06:30, Sun Oct 27, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 28 + πŸ•˜ (+0845) Australia/Eucla 09:45, Sun Oct 27, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 28 + πŸ•› (AEDT) Australia/Sydney 12:00, Sun Oct 27, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 28 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Sun Oct 27, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 28 + πŸ•’ (HST) Pacific/Honolulu 15:00, Sat Oct 26, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 27 + πŸ•— (CDT) US/Central 20:00, Sat Oct 26, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 27 + πŸ•˜ (CDT) Cuba 21:00, Sat Oct 26, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sun 27 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Sat Oct 26, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 27 +-- Hour before (2024-10-27T00:00:00Z = 1729987200) -- + + What time is it? + + πŸ•› (UTC) UTC 00:00, Sun Oct 27, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 27 + πŸ•‘ (CEST) Europe/Paris 02:00, Sun Oct 27, 2024 + 2 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + β‰ DST πŸ“† Mon 28 + πŸ•‘ (IST) Israel 02:00, Sun Oct 27, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 28 + πŸ•” (IST) Asia/Calcutta 05:30, Sun Oct 27, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 28 + πŸ•— (+0845) Australia/Eucla 08:45, Sun Oct 27, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 28 + πŸ•™ (AEDT) Australia/Sydney 11:00, Sun Oct 27, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 28 + πŸ•‘ (+14) Pacific/Kiritimati 14:00, Sun Oct 27, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 28 + πŸ•‘ (HST) Pacific/Honolulu 14:00, Sat Oct 26, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 27 + πŸ•– (CDT) US/Central 19:00, Sat Oct 26, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 27 + πŸ•— (CDT) Cuba 20:00, Sat Oct 26, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sun 27 + πŸ•˜ (-03) America/Argentina/ComodRivadavia 21:00, Sat Oct 26, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 27 +-- Hour after (2024-10-27T02:00:00Z = 1729994400) -- + + What time is it? + + πŸ•‘ (UTC) UTC 02:00, Sun Oct 27, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 27 + πŸ•’ (CET) Europe/Paris 03:00, Sun Oct 27, 2024 + 2 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + β‰ DST πŸ“† Mon 28 + πŸ•“ (IST) Israel 04:00, Sun Oct 27, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 28 + πŸ•– (IST) Asia/Calcutta 07:30, Sun Oct 27, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 28 + πŸ•™ (+0845) Australia/Eucla 10:45, Sun Oct 27, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 28 + πŸ• (AEDT) Australia/Sydney 13:00, Sun Oct 27, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 28 + πŸ•“ (+14) Pacific/Kiritimati 16:00, Sun Oct 27, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 28 + πŸ•“ (HST) Pacific/Honolulu 16:00, Sat Oct 26, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 27 + πŸ•˜ (CDT) US/Central 21:00, Sat Oct 26, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 27 + πŸ•™ (CDT) Cuba 22:00, Sat Oct 26, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sun 27 + πŸ•™ (-03) America/Argentina/ComodRivadavia 23:00, Sat Oct 26, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 27 +-- 3 days before (2024-10-24T01:00:00Z = 1729731600) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Thu Oct 24, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Thu 24 + πŸ•’ (CEST) Europe/Paris 03:00, Thu Oct 24, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Fri 25 + πŸ•“ (IDT) Israel 04:00, Thu Oct 24, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Fri 25 + πŸ•• (IST) Asia/Calcutta 06:30, Thu Oct 24, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Fri 25 + πŸ•˜ (+0845) Australia/Eucla 09:45, Thu Oct 24, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Fri 25 + πŸ•› (AEDT) Australia/Sydney 12:00, Thu Oct 24, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Fri 25 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Thu Oct 24, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Fri 25 + πŸ•’ (HST) Pacific/Honolulu 15:00, Wed Oct 23, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Thu 24 + πŸ•— (CDT) US/Central 20:00, Wed Oct 23, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Thu 24 + πŸ•˜ (CDT) Cuba 21:00, Wed Oct 23, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Thu 24 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Wed Oct 23, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Thu 24 +-- 2 days before (2024-10-25T01:00:00Z = 1729818000) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Fri Oct 25, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Fri 25 + πŸ•’ (CEST) Europe/Paris 03:00, Fri Oct 25, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Sat 26 + πŸ•“ (IDT) Israel 04:00, Fri Oct 25, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Sat 26 + πŸ•• (IST) Asia/Calcutta 06:30, Fri Oct 25, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Sat 26 + πŸ•˜ (+0845) Australia/Eucla 09:45, Fri Oct 25, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Sat 26 + πŸ•› (AEDT) Australia/Sydney 12:00, Fri Oct 25, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Sat 26 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Fri Oct 25, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sat 26 + πŸ•’ (HST) Pacific/Honolulu 15:00, Thu Oct 24, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Fri 25 + πŸ•— (CDT) US/Central 20:00, Thu Oct 24, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Fri 25 + πŸ•˜ (CDT) Cuba 21:00, Thu Oct 24, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Fri 25 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Thu Oct 24, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Fri 25 +-- 1 day before (2024-10-26T01:00:00Z = 1729904400) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Sat Oct 26, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sat 26 + πŸ•’ (CEST) Europe/Paris 03:00, Sat Oct 26, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Sun 27 + πŸ•“ (IDT) Israel 04:00, Sat Oct 26, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 1 + πŸ“† Sun 27β‰ DST + πŸ•• (IST) Asia/Calcutta 06:30, Sat Oct 26, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Sun 27 + πŸ•˜ (+0845) Australia/Eucla 09:45, Sat Oct 26, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Sun 27 + πŸ•› (AEDT) Australia/Sydney 12:00, Sat Oct 26, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Sun 27 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Sat Oct 26, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 27 + πŸ•’ (HST) Pacific/Honolulu 15:00, Fri Oct 25, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sat 26 + πŸ•— (CDT) US/Central 20:00, Fri Oct 25, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sat 26 + πŸ•˜ (CDT) Cuba 21:00, Fri Oct 25, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sat 26 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Fri Oct 25, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sat 26 +-- Day after (2024-10-28T01:00:00Z = 1730077200) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Mon Oct 28, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Mon 28 + πŸ•‘ (CET) Europe/Paris 02:00, Mon Oct 28, 2024 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Tue 29 + πŸ•’ (IST) Israel 03:00, Mon Oct 28, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Tue 29 + πŸ•• (IST) Asia/Calcutta 06:30, Mon Oct 28, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Tue 29 + πŸ•˜ (+0845) Australia/Eucla 09:45, Mon Oct 28, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Tue 29 + πŸ•› (AEDT) Australia/Sydney 12:00, Mon Oct 28, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Tue 29 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Mon Oct 28, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Tue 29 + πŸ•’ (HST) Pacific/Honolulu 15:00, Sun Oct 27, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 28 + πŸ•— (CDT) US/Central 20:00, Sun Oct 27, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Mon 28 + πŸ•˜ (CDT) Cuba 21:00, Sun Oct 27, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Mon 28 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Sun Oct 27, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Mon 28 diff --git a/testdata/view/test-dst-midnights.txt b/testdata/view/test-dst-midnights.txt new file mode 100644 index 0000000..ce122c5 --- /dev/null +++ b/testdata/view/test-dst-midnights.txt @@ -0,0 +1,20 @@ +-- Start DST missing midnight (2017-03-12T05:00:00Z = 1489294800) -- + + What time is it? + + πŸ•” (UTC) UTC 05:00, Sun Mar 12, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 12 + πŸ• (CDT) Cuba 01:00, Sun Mar 12, 2017 + 19 20 21 22 23 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sun 12=DST +-- End DST double midnight (2017-11-05T04:00:00Z = 1509854400) -- + + What time is it? + + πŸ•“ (UTC) UTC 04:00, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ•› (CDT) Cuba 00:00, Sun Nov 05, 2017 + 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05β‰ DST diff --git a/testdata/view/test-dst-start-days.txt b/testdata/view/test-dst-start-days.txt new file mode 100644 index 0000000..7ec5b00 --- /dev/null +++ b/testdata/view/test-dst-start-days.txt @@ -0,0 +1,278 @@ +When daylight saving starts, clocks go forward 1 hour, so the date has 23 hours not 24 hours. +Using UTC as the local timezone (simple hours from 0-23 with no daylight saving changes), +render before/after the start of March daylight saving in Europe (Sunday) and Israel (Friday). +This checks the following requirements: +- Alignment of hours between different timezones. +- Missing hours when DST starts (e.g. morning hour 2 should be missing in that zone, only). +- Abbreviated timezone names are updated before/after DST transitions: + - Central European Summer Time (CEST) versus CET (Central European Time) + - Israel Daylight Time (IDT) versus IST (Israel Standard Time) +- Timezones with same abbreviations but different offsets are handled correctly: + - IST: Israel Standard Time versus Indian Standard Time + - CDT: Central Daylight Time versus Cuba Daylight Time +- Timezones with positive and negative offsets. +- Timezones with different dates (before/after the International Date Line) +- Timezones with fractional offsets. +- Timezones with short and long names. +- Date transitions. +- Column location of showDates should shift by one hour between days before/after DST. + +-- Europe DST start (2024-03-31T01:00:00Z = 1711846800) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Sun Mar 31, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 31 + πŸ•’ (CEST) Europe/Paris 03:00, Sun Mar 31, 2024 + 1 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + =DST πŸ“† Mon 01 + πŸ•“ (IDT) Israel 04:00, Sun Mar 31, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Mon 01 + πŸ•• (IST) Asia/Calcutta 06:30, Sun Mar 31, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 01 + πŸ•˜ (+0845) Australia/Eucla 09:45, Sun Mar 31, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 01 + πŸ•› (AEDT) Australia/Sydney 12:00, Sun Mar 31, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 01 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Sun Mar 31, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 01 + πŸ•’ (HST) Pacific/Honolulu 15:00, Sat Mar 30, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 31 + πŸ•— (CDT) US/Central 20:00, Sat Mar 30, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 31 + πŸ•˜ (CDT) Cuba 21:00, Sat Mar 30, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sun 31 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Sat Mar 30, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 31 +-- Hour before (2024-03-31T00:00:00Z = 1711843200) -- + + What time is it? + + πŸ•› (UTC) UTC 00:00, Sun Mar 31, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 31 + πŸ• (CET) Europe/Paris 01:00, Sun Mar 31, 2024 + 1 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + =DST πŸ“† Mon 01 + πŸ•’ (IDT) Israel 03:00, Sun Mar 31, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Mon 01 + πŸ•” (IST) Asia/Calcutta 05:30, Sun Mar 31, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 01 + πŸ•— (+0845) Australia/Eucla 08:45, Sun Mar 31, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 01 + πŸ•™ (AEDT) Australia/Sydney 11:00, Sun Mar 31, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 01 + πŸ•‘ (+14) Pacific/Kiritimati 14:00, Sun Mar 31, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 01 + πŸ•‘ (HST) Pacific/Honolulu 14:00, Sat Mar 30, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 31 + πŸ•– (CDT) US/Central 19:00, Sat Mar 30, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 31 + πŸ•— (CDT) Cuba 20:00, Sat Mar 30, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sun 31 + πŸ•˜ (-03) America/Argentina/ComodRivadavia 21:00, Sat Mar 30, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 31 +-- Hour after (2024-03-31T02:00:00Z = 1711850400) -- + + What time is it? + + πŸ•‘ (UTC) UTC 02:00, Sun Mar 31, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 31 + πŸ•“ (CEST) Europe/Paris 04:00, Sun Mar 31, 2024 + 1 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + =DST πŸ“† Mon 01 + πŸ•” (IDT) Israel 05:00, Sun Mar 31, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Mon 01 + πŸ•– (IST) Asia/Calcutta 07:30, Sun Mar 31, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 01 + πŸ•™ (+0845) Australia/Eucla 10:45, Sun Mar 31, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 01 + πŸ• (AEDT) Australia/Sydney 13:00, Sun Mar 31, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 01 + πŸ•“ (+14) Pacific/Kiritimati 16:00, Sun Mar 31, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 01 + πŸ•“ (HST) Pacific/Honolulu 16:00, Sat Mar 30, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 31 + πŸ•˜ (CDT) US/Central 21:00, Sat Mar 30, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 31 + πŸ•™ (CDT) Cuba 22:00, Sat Mar 30, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sun 31 + πŸ•™ (-03) America/Argentina/ComodRivadavia 23:00, Sat Mar 30, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 31 +-- 3 days before (2024-03-28T01:00:00Z = 1711587600) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Thu Mar 28, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Thu 28 + πŸ•‘ (CET) Europe/Paris 02:00, Thu Mar 28, 2024 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Fri 29 + πŸ•’ (IST) Israel 03:00, Thu Mar 28, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Fri 29 + πŸ•• (IST) Asia/Calcutta 06:30, Thu Mar 28, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Fri 29 + πŸ•˜ (+0845) Australia/Eucla 09:45, Thu Mar 28, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Fri 29 + πŸ•› (AEDT) Australia/Sydney 12:00, Thu Mar 28, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Fri 29 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Thu Mar 28, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Fri 29 + πŸ•’ (HST) Pacific/Honolulu 15:00, Wed Mar 27, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Thu 28 + πŸ•— (CDT) US/Central 20:00, Wed Mar 27, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Thu 28 + πŸ•˜ (CDT) Cuba 21:00, Wed Mar 27, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Thu 28 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Wed Mar 27, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Thu 28 +-- 2 days before (2024-03-29T01:00:00Z = 1711674000) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Fri Mar 29, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Fri 29 + πŸ•‘ (CET) Europe/Paris 02:00, Fri Mar 29, 2024 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Sat 30 + πŸ•“ (IDT) Israel 04:00, Fri Mar 29, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + =DST πŸ“† Sat 30 + πŸ•• (IST) Asia/Calcutta 06:30, Fri Mar 29, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Sat 30 + πŸ•˜ (+0845) Australia/Eucla 09:45, Fri Mar 29, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Sat 30 + πŸ•› (AEDT) Australia/Sydney 12:00, Fri Mar 29, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Sat 30 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Fri Mar 29, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sat 30 + πŸ•’ (HST) Pacific/Honolulu 15:00, Thu Mar 28, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Fri 29 + πŸ•— (CDT) US/Central 20:00, Thu Mar 28, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Fri 29 + πŸ•˜ (CDT) Cuba 21:00, Thu Mar 28, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Fri 29 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Thu Mar 28, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Fri 29 +-- 1 day before (2024-03-30T01:00:00Z = 1711760400) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Sat Mar 30, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sat 30 + πŸ•‘ (CET) Europe/Paris 02:00, Sat Mar 30, 2024 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Sun 31 + πŸ•“ (IDT) Israel 04:00, Sat Mar 30, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Sun 31 + πŸ•• (IST) Asia/Calcutta 06:30, Sat Mar 30, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Sun 31 + πŸ•˜ (+0845) Australia/Eucla 09:45, Sat Mar 30, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Sun 31 + πŸ•› (AEDT) Australia/Sydney 12:00, Sat Mar 30, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Sun 31 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Sat Mar 30, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 31 + πŸ•’ (HST) Pacific/Honolulu 15:00, Fri Mar 29, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sat 30 + πŸ•— (CDT) US/Central 20:00, Fri Mar 29, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sat 30 + πŸ•˜ (CDT) Cuba 21:00, Fri Mar 29, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sat 30 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Fri Mar 29, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sat 30 +-- Day after (2024-04-01T01:00:00Z = 1711933200) -- + + What time is it? + + πŸ• (UTC) UTC 01:00, Mon Apr 01, 2024 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Mon 01 + πŸ•’ (CEST) Europe/Paris 03:00, Mon Apr 01, 2024 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Tue 02 + πŸ•“ (IDT) Israel 04:00, Mon Apr 01, 2024 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Tue 02 + πŸ•• (IST) Asia/Calcutta 06:30, Mon Apr 01, 2024 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Tue 02 + πŸ•˜ (+0845) Australia/Eucla 09:45, Mon Apr 01, 2024 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Tue 02 + πŸ•› (AEDT) Australia/Sydney 12:00, Mon Apr 01, 2024 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Tue 02 + πŸ•’ (+14) Pacific/Kiritimati 15:00, Mon Apr 01, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Tue 02 + πŸ•’ (HST) Pacific/Honolulu 15:00, Sun Mar 31, 2024 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 01 + πŸ•— (CDT) US/Central 20:00, Sun Mar 31, 2024 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Mon 01 + πŸ•˜ (CDT) Cuba 21:00, Sun Mar 31, 2024 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Mon 01 + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:00, Sun Mar 31, 2024 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Mon 01 diff --git a/testdata/view/test-fractional-timezone-offsets.txt b/testdata/view/test-fractional-timezone-offsets.txt new file mode 100644 index 0000000..c7088b1 --- /dev/null +++ b/testdata/view/test-fractional-timezone-offsets.txt @@ -0,0 +1,266 @@ +Check the following: +- Fractional timezones are displayed in the correct columns. +- Left & right navigation across fractional hours shifts the timelines correctly. +- Left & right navigation round-trip equals the starting point. +- Double-midnight for Cuba midnight DST end +- Crossing date boundaries + +-- Start time (2017-11-05T00:29:02Z = 1509841742) -- + + What time is it? + + πŸ•› (UTC) UTC 00:29, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ• (CET) Europe/Paris 01:29, Sun Nov 05, 2017 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Mon 06 + πŸ•‘ (IST) Israel 02:29, Sun Nov 05, 2017 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 06 + πŸ•” (IST) Asia/Calcutta 05:59, Sun Nov 05, 2017 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 06 + πŸ•˜ (+0845) Australia/Eucla 09:14, Sun Nov 05, 2017 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Mon 06 + πŸ•™ (AEDT) Australia/Sydney 11:29, Sun Nov 05, 2017 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 06 + πŸ•‘ (+14) Pacific/Kiritimati 14:29, Sun Nov 05, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•‘ (HST) Pacific/Honolulu 14:29, Sat Nov 04, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05 + πŸ•– (CDT) US/Central 19:29, Sat Nov 04, 2017 + 19 20 21 22 23 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sun 05β‰ DST + πŸ•— (CDT) Cuba 20:29, Sat Nov 04, 2017 + 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05β‰ DST + πŸ•˜ (-03) America/Argentina/ComodRivadavia 21:29, Sat Nov 04, 2017 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 05 +-- Plus 1 hour (2017-11-05T01:29:02Z = 1509845342) -- + + What time is it? + + πŸ• (UTC) UTC 01:29, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ•‘ (CET) Europe/Paris 02:29, Sun Nov 05, 2017 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Mon 06 + πŸ•’ (IST) Israel 03:29, Sun Nov 05, 2017 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 06 + πŸ•• (IST) Asia/Calcutta 06:59, Sun Nov 05, 2017 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 06 + πŸ•™ (+0845) Australia/Eucla 10:14, Sun Nov 05, 2017 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Mon 06 + πŸ•› (AEDT) Australia/Sydney 12:29, Sun Nov 05, 2017 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 06 + πŸ•’ (+14) Pacific/Kiritimati 15:29, Sun Nov 05, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•’ (HST) Pacific/Honolulu 15:29, Sat Nov 04, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05 + πŸ•— (CDT) US/Central 20:29, Sat Nov 04, 2017 + 19 20 21 22 23 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sun 05β‰ DST + πŸ•˜ (CDT) Cuba 21:29, Sat Nov 04, 2017 + 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05β‰ DST + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:29, Sat Nov 04, 2017 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 05 +-- Minus 15 minutes (2017-11-05T01:14:02Z = 1509844442) -- + + What time is it? + + πŸ• (UTC) UTC 01:14, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ•‘ (CET) Europe/Paris 02:14, Sun Nov 05, 2017 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Mon 06 + πŸ•’ (IST) Israel 03:14, Sun Nov 05, 2017 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 06 + πŸ•• (IST) Asia/Calcutta 06:44, Sun Nov 05, 2017 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 06 + πŸ•˜ (+0845) Australia/Eucla 09:59, Sun Nov 05, 2017 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 06 + πŸ•› (AEDT) Australia/Sydney 12:14, Sun Nov 05, 2017 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 06 + πŸ•’ (+14) Pacific/Kiritimati 15:14, Sun Nov 05, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•’ (HST) Pacific/Honolulu 15:14, Sat Nov 04, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05 + πŸ•— (CDT) US/Central 20:14, Sat Nov 04, 2017 + 19 20 21 22 23 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sun 05β‰ DST + πŸ•˜ (CDT) Cuba 21:14, Sat Nov 04, 2017 + 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05β‰ DST + πŸ•™ (-03) America/Argentina/ComodRivadavia 22:14, Sat Nov 04, 2017 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 05 +-- Return to start (2017-11-05T00:29:02Z = 1509841742) -- + + What time is it? + + πŸ•› (UTC) UTC 00:29, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ• (CET) Europe/Paris 01:29, Sun Nov 05, 2017 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Mon 06 + πŸ•‘ (IST) Israel 02:29, Sun Nov 05, 2017 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 06 + πŸ•” (IST) Asia/Calcutta 05:59, Sun Nov 05, 2017 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 06 + πŸ•˜ (+0845) Australia/Eucla 09:14, Sun Nov 05, 2017 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Mon 06 + πŸ•™ (AEDT) Australia/Sydney 11:29, Sun Nov 05, 2017 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 06 + πŸ•‘ (+14) Pacific/Kiritimati 14:29, Sun Nov 05, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•‘ (HST) Pacific/Honolulu 14:29, Sat Nov 04, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05 + πŸ•– (CDT) US/Central 19:29, Sat Nov 04, 2017 + 19 20 21 22 23 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sun 05β‰ DST + πŸ•— (CDT) Cuba 20:29, Sat Nov 04, 2017 + 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05β‰ DST + πŸ•˜ (-03) America/Argentina/ComodRivadavia 21:29, Sat Nov 04, 2017 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 05 +-- Minus 1 hour, date changed (2017-11-04T23:29:02Z = 1509838142) -- + + What time is it? + + πŸ•™ (UTC) UTC 23:29, Sat Nov 04, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sat 04 + πŸ•› (CET) Europe/Paris 00:29, Sun Nov 05, 2017 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Sun 05 + πŸ• (IST) Israel 01:29, Sun Nov 05, 2017 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Sun 05 + πŸ•“ (IST) Asia/Calcutta 04:59, Sun Nov 05, 2017 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Sun 05 + πŸ•— (+0845) Australia/Eucla 08:14, Sun Nov 05, 2017 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Sun 05 + πŸ•™ (AEDT) Australia/Sydney 10:29, Sun Nov 05, 2017 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Sun 05 + πŸ• (+14) Pacific/Kiritimati 13:29, Sun Nov 05, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05 + πŸ• (HST) Pacific/Honolulu 13:29, Sat Nov 04, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sat 04 + πŸ•• (CDT) US/Central 18:29, Sat Nov 04, 2017 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sat 04 + πŸ•– (CDT) Cuba 19:29, Sat Nov 04, 2017 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sat 04 + πŸ•— (-03) America/Argentina/ComodRivadavia 20:29, Sat Nov 04, 2017 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sat 04 +-- Return to start (2017-11-05T00:29:02Z = 1509841742) -- + + What time is it? + + πŸ•› (UTC) UTC 00:29, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ• (CET) Europe/Paris 01:29, Sun Nov 05, 2017 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Mon 06 + πŸ•‘ (IST) Israel 02:29, Sun Nov 05, 2017 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 06 + πŸ•” (IST) Asia/Calcutta 05:59, Sun Nov 05, 2017 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 06 + πŸ•˜ (+0845) Australia/Eucla 09:14, Sun Nov 05, 2017 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Mon 06 + πŸ•™ (AEDT) Australia/Sydney 11:29, Sun Nov 05, 2017 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 06 + πŸ•‘ (+14) Pacific/Kiritimati 14:29, Sun Nov 05, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•‘ (HST) Pacific/Honolulu 14:29, Sat Nov 04, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05 + πŸ•– (CDT) US/Central 19:29, Sat Nov 04, 2017 + 19 20 21 22 23 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sun 05β‰ DST + πŸ•— (CDT) Cuba 20:29, Sat Nov 04, 2017 + 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05β‰ DST + πŸ•˜ (-03) America/Argentina/ComodRivadavia 21:29, Sat Nov 04, 2017 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 05 +-- Zero minutes (2017-11-05T00:00:00Z = 1509840000) -- + + What time is it? + + πŸ•› (UTC) UTC 00:00, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ• (CET) Europe/Paris 01:00, Sun Nov 05, 2017 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Mon 06 + πŸ•‘ (IST) Israel 02:00, Sun Nov 05, 2017 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 06 + πŸ•” (IST) Asia/Calcutta 05:30, Sun Nov 05, 2017 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 06 + πŸ•— (+0845) Australia/Eucla 08:45, Sun Nov 05, 2017 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 06 + πŸ•™ (AEDT) Australia/Sydney 11:00, Sun Nov 05, 2017 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 06 + πŸ•‘ (+14) Pacific/Kiritimati 14:00, Sun Nov 05, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•‘ (HST) Pacific/Honolulu 14:00, Sat Nov 04, 2017 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05 + πŸ•– (CDT) US/Central 19:00, Sat Nov 04, 2017 + 19 20 21 22 23 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sun 05β‰ DST + πŸ•— (CDT) Cuba 20:00, Sat Nov 04, 2017 + 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05β‰ DST + πŸ•˜ (-03) America/Argentina/ComodRivadavia 21:00, Sat Nov 04, 2017 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 05 diff --git a/testdata/view/test-highlight-markers.txt b/testdata/view/test-highlight-markers.txt new file mode 100644 index 0000000..b81fa1e --- /dev/null +++ b/testdata/view/test-highlight-markers.txt @@ -0,0 +1,7 @@ +-- Highlight Local Zone -- + + What time is it? + +>>πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 +>> 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 +>>πŸ“† Sun 05 diff --git a/testdata/view/test-local-timezones.txt b/testdata/view/test-local-timezones.txt new file mode 100644 index 0000000..d7d98b0 --- /dev/null +++ b/testdata/view/test-local-timezones.txt @@ -0,0 +1,261 @@ +Test local timezones other than UTC, with UTC as a reference for consistency. +Checking: +- Fractional-hour timezones +- Daylight saving peculiarities + +How to read the results (this is the method used in view_test.go’s TestLocalTimezones): +- Numbered as [group][test] so within a [group] we check that all [test] are β€˜aligned’ +- Read the β€˜alignment’ by looking at the date-time in each test title and finding its column + within the test (be sure you look on the correct side of any date line), then check that all + the hours in the columns _below_ Local are the same for all tests within the group. + For example: + - First test [0][0] is timestamped midnight UTC, which is the first column on the left. + - Second test [0][1] is timestamped 5:30 am in India, which is the sixth column from the left. + - Third test [0][2] is timestamped 8:00 pm in Cuba, which is the 4th column from the right. + - These three columns should show the same hours vertically. +- Although there are other ways of checking it, these may be harder because of differences + caused by daylight-saving offsets on either side of midnight. + +Note: The expectation for test case [1][2] (Cuba) is an exceptional corner case, +because the DST-ending midnight is not the first hour of day, hence no πŸ“† icon. + +-- [0][0] UTC (2017-11-05T00:00:00Z = 1509840000) -- + + What time is it? + + πŸ•› [+00:00] (UTC) Local 2017-11-05T00:00+00:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ•› [+00:00] (UTC) UTC 2017-11-05T00:00+00:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ• [+01:00] (CET) Europe/Paris 2017-11-05T01:00+01:00 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Mon 06 + πŸ•‘ [+02:00] (IST) Israel 2017-11-05T02:00+02:00 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 06 + πŸ•” [+05:30] (IST) Asia/Calcutta 2017-11-05T05:30+05:30 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 06 + πŸ•— [+08:45] (+0845) Australia/Eucla 2017-11-05T08:45+08:45 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 06 + πŸ•™ [+11:00] (AEDT) Australia/Sydney 2017-11-05T11:00+11:00 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 06 + πŸ•‘ [+14:00] (+14) Pacific/Kiritimati 2017-11-05T14:00+14:00 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•‘ [-10:00] (HST) Pacific/Honolulu 2017-11-04T14:00-10:00 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05 + πŸ•– [-05:00] (CDT) US/Central 2017-11-04T19:00-05:00 + 19 20 21 22 23 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sun 05β‰ DST + πŸ•— [-04:00] (CDT) Cuba 2017-11-04T20:00-04:00 + 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05β‰ DST + πŸ•˜ [-03:00] (-03) America/Argentina/ComodRivadavia 2017-11-04T21:00-03:00 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 05 +-- [0][1] Asia/Calcutta (2017-11-05T05:30:00+05:30 = 1509840000) -- + + What time is it? + + πŸ•” [+00:00] (IST) Local 2017-11-05T05:30+05:30 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ•› [-05:30] (UTC) UTC 2017-11-05T00:00+00:00 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05 + πŸ• [-04:30] (CET) Europe/Paris 2017-11-05T01:00+01:00 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Sun 05 + πŸ•‘ [-03:30] (IST) Israel 2017-11-05T02:00+02:00 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Sun 05 + πŸ•” [+00:00] (IST) Asia/Calcutta 2017-11-05T05:30+05:30 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 + πŸ•— [+03:15] (+0845) Australia/Eucla 2017-11-05T08:45+08:45 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Mon 06 + πŸ•™ [+05:30] (AEDT) Australia/Sydney 2017-11-05T11:00+11:00 + 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 + πŸ“† Mon 06 + πŸ•‘ [+08:30] (+14) Pacific/Kiritimati 2017-11-05T14:00+14:00 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Mon 06 + πŸ•‘ [-15:30] (HST) Pacific/Honolulu 2017-11-04T14:00-10:00 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Sun 05 + πŸ•– [-10:30] (CDT) US/Central 2017-11-04T19:00-05:00 + 14 15 16 17 18 19 20 21 22 23 0 1 1 2 3 4 5 6 7 8 9 10 11 12 + πŸ“† Sun 05β‰ DST + πŸ•— [-09:30] (CDT) Cuba 2017-11-04T20:00-04:00 + 15 16 17 18 19 20 21 22 23 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Sun 05β‰ DST + πŸ•˜ [-08:30] (-03) America/Argentina/ComodRivadavia 2017-11-04T21:00-03:00 + 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + πŸ“† Sun 05 +-- [0][2] Cuba (2017-11-04T20:00:00-04:00 = 1509840000) -- + + What time is it? + + πŸ•— [+00:00] (CDT) Local 2017-11-04T20:00-04:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sat 04 + πŸ•› [+04:00] (UTC) UTC 2017-11-05T00:00+00:00 + 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 + πŸ“† Sun 05 + πŸ• [+05:00] (CET) Europe/Paris 2017-11-05T01:00+01:00 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Sun 05 + πŸ•‘ [+06:00] (IST) Israel 2017-11-05T02:00+02:00 + 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 + πŸ“† Sun 05 + πŸ•” [+09:30] (IST) Asia/Calcutta 2017-11-05T05:30+05:30 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Sun 05 + πŸ•— [+12:45] (+0845) Australia/Eucla 2017-11-05T08:45+08:45 + 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 + πŸ“† Sun 05 + πŸ•™ [+15:00] (AEDT) Australia/Sydney 2017-11-05T11:00+11:00 + 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + πŸ“† Sun 05 + πŸ•‘ [+18:00] (+14) Pacific/Kiritimati 2017-11-05T14:00+14:00 + 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sun 05 + πŸ•‘ [-06:00] (HST) Pacific/Honolulu 2017-11-04T14:00-10:00 + 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Sat 04 + πŸ•– [-01:00] (CDT) US/Central 2017-11-04T19:00-05:00 + 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + πŸ“† Sat 04 + πŸ•— [+00:00] (CDT) Cuba 2017-11-04T20:00-04:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sat 04 + πŸ•˜ [+01:00] (-03) America/Argentina/ComodRivadavia 2017-11-04T21:00-03:00 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Sun 05 +-- [1][0] UTC (2017-11-06T00:30:00Z = 1509928200) -- + + What time is it? + + πŸ•› [+00:00] (UTC) Local 2017-11-06T00:30+00:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Mon 06 + πŸ•› [+00:00] (UTC) UTC 2017-11-06T00:30+00:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Mon 06 + πŸ• [+01:00] (CET) Europe/Paris 2017-11-06T01:30+01:00 + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 + πŸ“† Tue 07 + πŸ•‘ [+02:00] (IST) Israel 2017-11-06T02:30+02:00 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Tue 07 + πŸ•• [+05:30] (IST) Asia/Calcutta 2017-11-06T06:00+05:30 + 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 + πŸ“† Tue 07 + πŸ•˜ [+08:45] (+0845) Australia/Eucla 2017-11-06T09:15+08:45 + 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 + πŸ“† Tue 07 + πŸ•™ [+11:00] (AEDT) Australia/Sydney 2017-11-06T11:30+11:00 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Tue 07 + πŸ•‘ [+14:00] (+14) Pacific/Kiritimati 2017-11-06T14:30+14:00 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Tue 07 + πŸ•‘ [-10:00] (HST) Pacific/Honolulu 2017-11-05T14:30-10:00 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•• [-06:00] (CST) US/Central 2017-11-05T18:30-06:00 + 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Mon 06 + πŸ•– [-05:00] (CST) Cuba 2017-11-05T19:30-05:00 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Mon 06 + πŸ•˜ [-03:00] (-03) America/Argentina/ComodRivadavia 2017-11-05T21:30-03:00 + 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + πŸ“† Mon 06 +-- [1][1] Asia/Calcutta (2017-11-06T06:00:00+05:30 = 1509928200) -- + + What time is it? + + πŸ•• [+00:00] (IST) Local 2017-11-06T06:00+05:30 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Mon 06 + πŸ•› [-05:30] (UTC) UTC 2017-11-06T00:30+00:00 + 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + πŸ“† Mon 06 + πŸ• [-04:30] (CET) Europe/Paris 2017-11-06T01:30+01:00 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Mon 06 + πŸ•‘ [-03:30] (IST) Israel 2017-11-06T02:30+02:00 + 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + πŸ“† Mon 06 + πŸ•• [+00:00] (IST) Asia/Calcutta 2017-11-06T06:00+05:30 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Mon 06 + πŸ•˜ [+03:15] (+0845) Australia/Eucla 2017-11-06T09:15+08:45 + 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 + πŸ“† Tue 07 + πŸ•™ [+05:30] (AEDT) Australia/Sydney 2017-11-06T11:30+11:00 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Tue 07 + πŸ•‘ [+08:30] (+14) Pacific/Kiritimati 2017-11-06T14:30+14:00 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Tue 07 + πŸ•‘ [-15:30] (HST) Pacific/Honolulu 2017-11-05T14:30-10:00 + 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 + πŸ“† Mon 06 + πŸ•• [-11:30] (CST) US/Central 2017-11-05T18:30-06:00 + 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 + πŸ“† Mon 06 + πŸ•– [-10:30] (CST) Cuba 2017-11-05T19:30-05:00 + 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 + πŸ“† Mon 06 + πŸ•˜ [-08:30] (-03) America/Argentina/ComodRivadavia 2017-11-05T21:30-03:00 + 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + πŸ“† Mon 06 +-- [1][2] Cuba (2017-11-05T19:30:00-05:00 = 1509928200) -- + + What time is it? + + πŸ•– [+00:00] (CST) Local 2017-11-05T19:30-05:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + β‰ DST + πŸ•› [+05:00] (UTC) UTC 2017-11-06T00:30+00:00 + 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 + πŸ“† Mon 06 + πŸ• [+06:00] (CET) Europe/Paris 2017-11-06T01:30+01:00 + 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 + πŸ“† Mon 06 + πŸ•‘ [+07:00] (IST) Israel 2017-11-06T02:30+02:00 + 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 + πŸ“† Mon 06 + πŸ•• [+10:30] (IST) Asia/Calcutta 2017-11-06T06:00+05:30 + 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 + πŸ“† Mon 06 + πŸ•˜ [+13:45] (+0845) Australia/Eucla 2017-11-06T09:15+08:45 + 14 15 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + πŸ“† Mon 06 + πŸ•™ [+16:00] (AEDT) Australia/Sydney 2017-11-06T11:30+11:00 + 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + πŸ“† Mon 06 + πŸ•‘ [+19:00] (+14) Pacific/Kiritimati 2017-11-06T14:30+14:00 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Mon 06 + πŸ•‘ [-05:00] (HST) Pacific/Honolulu 2017-11-05T14:30-10:00 + 19 20 21 22 23 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + πŸ“† Sun 05 + πŸ•• [-01:00] (CST) US/Central 2017-11-05T18:30-06:00 + 0 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + πŸ“† Sun 05β‰ DST + πŸ•– [+00:00] (CST) Cuba 2017-11-05T19:30-05:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + β‰ DST + πŸ•˜ [+02:00] (-03) America/Argentina/ComodRivadavia 2017-11-05T21:30-03:00 + 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 1 + πŸ“† Mon 06 diff --git a/testdata/view/test-right-alignment.txt b/testdata/view/test-right-alignment.txt index a3add25..9bfcc36 100644 --- a/testdata/view/test-right-alignment.txt +++ b/testdata/view/test-right-alignment.txt @@ -1,25 +1,25 @@ -- expected -- - πŸ•› UTC 00:01, Sun Nov 05, 2017 - πŸ•› UTC 00:01, Sun Nov 05, 2017 - πŸ•› UTC 00:01, Sun Nov 05, 2017 + πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 + πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 + πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 -- observed: narrow -- What time is it? - πŸ•› UTC 00:01, Sun Nov 05, 2017 + πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 πŸ“† Sun 05 -- observed: medium -- What time is it? - πŸ•› UTC 00:01, Sun Nov 05, 2017 + πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 πŸ“† Sun 05 -- observed: wide -- What time is it? - πŸ•› UTC 00:01, Sun Nov 05, 2017 + πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 πŸ“† Sun 05 diff --git a/testdata/view/test-time-formats.txt b/testdata/view/test-time-formats.txt new file mode 100644 index 0000000..4bea86d --- /dev/null +++ b/testdata/view/test-time-formats.txt @@ -0,0 +1,25 @@ +-- expected -- +00:01, Sun Nov 05, 2017 +2017-11-05T00:01+00:00 +No DST, Week 44, Day 309, Unix 1509840062 +-- observed: DefaultFormatStyle -- + + What time is it? + + πŸ•› (UTC) UTC 00:01, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 +-- observed: IsoFormatStyle -- + + What time is it? + + πŸ•› (UTC) UTC 2017-11-05T00:01+00:00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 +-- observed: UnixFormatStyle -- + + What time is it? + + πŸ•› (UTC) UTC No DST, Week 44, Day 309, Unix 1509840062 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 diff --git a/testdata/view/test-zone-styles.txt b/testdata/view/test-zone-styles.txt new file mode 100644 index 0000000..5535eeb --- /dev/null +++ b/testdata/view/test-zone-styles.txt @@ -0,0 +1,25 @@ +-- expected -- +πŸ•› (UTC) UTC +πŸ•› [Z+00:00] (UTC) UTC +πŸ•› [+00:00] (UTC) UTC +-- observed: AbbreviationZoneStyle -- + + What time is it? + + πŸ•› (UTC) UTC 12:01AM, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 +-- observed: WithZOffsetZoneStyle -- + + What time is it? + + πŸ•› [Z+00:00] (UTC) UTC 12:01AM, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 +-- observed: WithRelativeZoneStyle -- + + What time is it? + + πŸ•› [+00:00] (UTC) UTC 12:01AM, Sun Nov 05, 2017 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + πŸ“† Sun 05 diff --git a/view.go b/view.go index 8e7ce1b..9625659 100644 --- a/view.go +++ b/view.go @@ -21,11 +21,72 @@ import ( "os" "strconv" "strings" + "time" "github.com/muesli/termenv" xterm "golang.org/x/term" ) +type FormatStyle int + +const ( + DefaultFormatStyle FormatStyle = iota + IsoFormatStyle + UnixFormatStyle +) + +func (fs FormatStyle) next() FormatStyle { + switch (fs) { + case DefaultFormatStyle: + return IsoFormatStyle + case IsoFormatStyle: + return UnixFormatStyle + default: + return DefaultFormatStyle + } +} + +func (fs FormatStyle) previous() FormatStyle { + switch (fs) { + case DefaultFormatStyle: + return UnixFormatStyle + case UnixFormatStyle: + return IsoFormatStyle + default: + return DefaultFormatStyle + } +} + +type ZoneStyle int + +const ( + AbbreviationZoneStyle ZoneStyle = iota + WithZOffsetZoneStyle + WithRelativeZoneStyle +) + +func (zs ZoneStyle) next() ZoneStyle { + switch (zs) { + case AbbreviationZoneStyle: + return WithZOffsetZoneStyle + case WithZOffsetZoneStyle: + return WithRelativeZoneStyle + default: + return AbbreviationZoneStyle + } +} + +func (zs ZoneStyle) previous() ZoneStyle { + switch (zs) { + case AbbreviationZoneStyle: + return WithRelativeZoneStyle + case WithRelativeZoneStyle: + return WithZOffsetZoneStyle + default: + return AbbreviationZoneStyle + } +} + // Width required to display 24 hours const UIWidth = 94 const MinimumZoneHeaderPadding = 6 @@ -48,25 +109,39 @@ func (m model) View() string { } } + midnight := time.Date( + m.clock.t.Year(), + m.clock.t.Month(), + m.clock.t.Day(), + 0, // Hours + m.clock.t.Minute(), + 0, // Seconds + 0, // Nanoseconds + m.clock.t.Location(), + ) + midnightOffset := time.Duration(m.clock.t.UnixNano() - midnight.UnixNano()) + cursorColumn := int(midnightOffset / time.Hour) + // Show hours for each zone - for zi, zone := range m.zones { + for i, zone := range m.zones { hours := strings.Builder{} dates := strings.Builder{} - currentTime := zone.currentTime(m.clock.t) - - startHour := 0 - if zi > 0 { - startHour = (currentTime.Hour() - m.zones[0].currentTime(m.clock.t).Hour()) % 24 - } + timeInZone := zone.currentTime(m.clock.t) + midnightInZone := timeInZone.Add(-midnightOffset) + wasDST := midnightInZone.Add(-time.Hour).IsDST() + previousHour := midnightInZone.Add(-time.Hour).Hour() + highlighted := i == (m.highlighted - 1) dateChanged := false - for i := startHour; i < startHour+24; i++ { - hour := ((i % 24) + 24) % 24 // mod 24 + for column := 0; column < 24; column++ { + time := midnightInZone.Add(time.Duration(column) * time.Hour) + nowDST := time.IsDST() + hour := time.Hour() out := termenv.String(fmt.Sprintf("%2d", hour)) out = out.Foreground(term.Color(hourColorCode(hour))) // Cursor - if m.clock.t.Hour() == i-startHour { + if column == cursorColumn { out = out.Background(term.Color(hourColorCode(hour))) if hasDarkBackground { out = out.Foreground(term.Color("#262626")).Bold() @@ -78,33 +153,77 @@ func (m model) View() string { hours.WriteString(" ") // Show the day under the hour, when the date changes. - if !m.showDates { - continue - } - if hour == 0 { - dates.WriteString(formatDayChange(&m, zone)) - dateChanged = true - } - if !dateChanged { - dates.WriteString(" ") + if m.showDates { + if hour < previousHour { + dates.WriteString(formatDayChange(&m, zone)) + dateChanged = true + } + + if wasDST != nowDST { + if nowDST { + dates.WriteString("=DST") + } else { + dates.WriteString("β‰ DST") + } + } else if !dateChanged { + dates.WriteString(" ") + } } + + wasDST = nowDST + previousHour = hour } var datetime string - if m.isMilitary { - datetime = zone.ShortMT(m.clock.t) - } else { - datetime = zone.ShortDT(m.clock.t) + switch m.formatStyle { + case IsoFormatStyle: + datetime = timeInZone.Format("2006-01-02T15:04-07:00") + case UnixFormatStyle: + _, weekOfYear := timeInZone.ISOWeek() + dayOfYear := timeInZone.Format("__2") + yesNo := map[bool]string{true: "With", false: "No"} + datetime = fmt.Sprintf( + "%v DST, Week %v, Day %v, Unix %v", + yesNo[timeInZone.IsDST()], + weekOfYear, + dayOfYear, + timeInZone.Unix(), + ) + default: + if m.isMilitary { + datetime = zone.ShortMT(m.clock.t) + } else { + datetime = zone.ShortDT(m.clock.t) + } + } + + var zoneString = zone.VerboseString(timeInZone) + switch m.zoneStyle { + case WithZOffsetZoneStyle: + utcOffset := timeInZone.Format("Z-07:00") + zoneString = fmt.Sprintf("[%s] %s", utcOffset, zoneString) + case WithRelativeZoneStyle: + _, otherOffset := timeInZone.Zone() + _, localOffset := m.clock.t.Zone() + relativeOffset := m.clock.t.In(time.FixedZone("", otherOffset - localOffset)).Format("-07:00") + zoneString = fmt.Sprintf("[%s] %s", relativeOffset, zoneString) + default: } clockString := zone.ClockEmoji(m.clock.t) - zoneString := zone.String() usedZoneHeaderWidth := termenv.String(clockString + zoneString + datetime).Width() unusedZoneHeaderWidth := max(0, zoneHeaderWidth - usedZoneHeaderWidth - MinimumZoneHeaderPadding) rightAlignmentSpace := strings.Repeat(" ", unusedZoneHeaderWidth) zoneHeader := fmt.Sprintf("%s %s %s%s", clockString, normalTextStyle(zoneString), rightAlignmentSpace, dateTimeStyle(datetime)) - s += fmt.Sprintf(" %s\n %s\n %s\n", zoneHeader, hours.String(), dates.String()) + marker := " " + if highlighted { + marker = termenv.String(">>").Reverse().String() + } + lines := []string{zoneHeader, hours.String(), dates.String()} + for _, line := range lines { + s += fmt.Sprintf("%s%s\n", marker, line) + } } if m.interactive { @@ -113,36 +232,51 @@ func (m model) View() string { return s } -// Generate the string -func generateKeymapString(k Keymaps) string { - return fmt.Sprintf(", %s/%s: hours, %s/%s: days, %s/%s: weeks, %s: toggle date, %s: now, %s: open in web", - // Only use the first of each mapping - k.PrevHour[0], k.NextHour[0], - k.PrevDay[0], k.NextDay[0], - k.PrevWeek[0], k.NextWeek[0], - k.ToggleDate[0], - k.Now[0], - k.OpenWeb[0], - ) -} - -func status(m model) string { - var text string - - helpPrefix := " q: quit, ?: help" +// Generate the help lines +func generateKeymapStrings(k Keymaps, showAll bool) []string { + helpKey := fmt.Sprintf("%s: help", k.Help[0]) + quitKey := fmt.Sprintf("%s: quit", k.Quit[0]) - if m.showHelp { - text = helpPrefix + generateKeymapString(m.keymaps) + if showAll { + delimiter := ", " + return []string { + strings.Join( + []string { + helpKey, + fmt.Sprintf("%s/%s/%s: minutes", k.PrevMinute[0], k.NextMinute[0], k.ZeroMinute[0]), + fmt.Sprintf("%s/%s: hours", k.PrevHour[0], k.NextHour[0]), + fmt.Sprintf("%s/%s: days", k.PrevDay[0], k.NextDay[0]), + fmt.Sprintf("%s/%s: weeks", k.PrevWeek[0], k.NextWeek[0]), + fmt.Sprintf("%s: go to now", k.Now[0]), + fmt.Sprintf("%s/%s: highlight", k.NextLine[0], k.PrevLine[0]), + }, + delimiter, + ), + strings.Join( + []string { + quitKey, + fmt.Sprintf("%s: toggle dates", k.ToggleDate[0]), + fmt.Sprintf("%s: toggle formats", k.NextFStyle[0]), + fmt.Sprintf("%s: toggle zone offsets", k.NextZStyle[0]), + fmt.Sprintf("%s: open in web", k.OpenWeb[0]), + }, + delimiter, + ), + } } else { - text = helpPrefix + return []string { + helpKey, + quitKey, + } } +} - for { - text += " " - if len(text) > UIWidth { - text = text[0:UIWidth] - break - } +func status(m model) string { + var text []string = generateKeymapStrings(m.keymaps, m.showHelp) + + backgroundPadding := strings.Repeat(" ", UIWidth) + for i, line := range text { + text[i] = (" " + line + backgroundPadding)[:UIWidth] } color := "#939183" @@ -150,7 +284,7 @@ func status(m model) string { color = "#605C5A" } - status := termenv.String(text).Foreground(term.Color(color)) + status := termenv.String(strings.Join(text, "\n")).Foreground(term.Color(color)) return status.String() } diff --git a/view_test.go b/view_test.go index 44c83fc..28b991c 100644 --- a/view_test.go +++ b/view_test.go @@ -17,14 +17,416 @@ package main import ( + "fmt" "os" - "strconv" "strings" "testing" + "time" + tea "github.com/charmbracelet/bubbletea" "golang.org/x/tools/txtar" ) +var ( + dstTestZones []*Zone = nil +) + +func LoadDstTestZones(t *testing.T) []*Zone { + if dstTestZones == nil { + config, err := LoadDefaultConfig([]string{ + "UTC", // Z + "Europe/Paris", // Z+1 (CET), Z+2 (CEST) + "Israel", // Z+2 (IST), Z+3 (IDT) + "Asia/Calcutta", // Z+5:30 (IST) + "Australia/Eucla", // Z+8:45 (no abbreviation) + "Australia/Sydney", // Z+10 (AEST), Z+11 (AEDT) + "Pacific/Kiritimati", // Z+14 (no abbreviation) + "Pacific/Honolulu", // Z-10 (HST) + "US/Central", // Z-6 (CST), Z-5 (CDT) + "Cuba", // Z-5 (CST), Z-4 (CDT) + "America/Argentina/ComodRivadavia", // Z-3 (no abbreviation) + }) + if err != nil { + t.Fatal(err) + } + dstTestZones = config.Zones[1:] + } + return dstTestZones +} + +func RunDstDaysTest( + t *testing.T, + title string, + testDataFile string, + transition time.Time, +) { + testData, err := txtar.ParseFile(testDataFile) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + time time.Time + }{ + {title, transition}, + {"Hour before", transition.Add(-time.Hour)}, + {"Hour after", transition.Add(time.Hour)}, + {"3 days before", transition.Add(-72 * time.Hour)}, + {"2 days before", transition.Add(-48 * time.Hour)}, + {"1 day before", transition.Add(-24 * time.Hour)}, + {"Day after", transition.Add(24 * time.Hour)}, + } + + var observations []string + var outputData = []txtar.File{} + for _, test := range tests { + state := model{ + zones: LoadDstTestZones(t), + clock: *NewClockTime(test.time), + isMilitary: true, + showDates: true, + } + observed := stripAnsiControlSequences(state.View()) + observations = append(observations, observed) + outputData = append( + outputData, + txtar.File{ + Name: fmt.Sprintf("%v (%v = %v)", test.name, test.time.Format(time.RFC3339), test.time.Unix()), + Data: []byte(observed), + }, + ) + } + + archive := txtar.Archive{ + Comment: testData.Comment, + Files: outputData, + } + os.WriteFile(testDataFile, txtar.Format(&archive), 0666) + + for i, test := range tests { + expected := stripAnsiControlSequencesAndNewline(testData.Files[i].Data) + observed := stripAnsiControlSequencesAndNewline(outputData[i].Data) + if expected != observed { + t.Errorf("Daylight Saving: Mismatched %s: Check git diff %s", test.name, testDataFile) + } + } +} + +func TestDstEndDays(t *testing.T) { + testDataFile := "testdata/view/test-dst-end-days.txt" + europeEndDst := time.Date(2024, time.October, 27, 1, 0, 0, 0, time.UTC) + RunDstDaysTest(t, "Europe DST end", testDataFile, europeEndDst) +} + +func TestDstStartDays(t *testing.T) { + testDataFile := "testdata/view/test-dst-start-days.txt" + europeStartDst := time.Date(2024, time.March, 31, 1, 0, 0, 0, time.UTC) + RunDstDaysTest(t, "Europe DST start", testDataFile, europeStartDst) +} + +func TestDstSpecialMidnights(t *testing.T) { + // The following are expressed in UTC, because the local date boundary is unusual: + cubaDstStart := time.Date(2017, time.March, 12, 5, 0, 0, 0, time.UTC) // 12 Mar Cuba missing midnight + cubaDstEnd := time.Date(2017, time.November, 5, 4, 0, 0, 0, time.UTC) // 5 Mar Cuba double midnight + + testDataFile := "testdata/view/test-dst-midnights.txt" + testData, err := txtar.ParseFile(testDataFile) + if err != nil { + t.Fatal(err) + } + + config, err := LoadDefaultConfig([]string{ + "UTC", // Z + "Cuba", // Z-5 (CST), Z-4 (CDT) + }) + if err != nil { + t.Fatal(err) + } + midnightTestZones := config.Zones[1:] + + tests := []struct { + name string + time time.Time + }{ + {"Start DST missing midnight", cubaDstStart}, + {"End DST double midnight", cubaDstEnd}, + } + + var outputData = make([]txtar.File, len(tests)) + for i, test := range tests { + state := model{ + zones: midnightTestZones, + clock: *NewClockTime(test.time), + isMilitary: true, + showDates: true, + } + observed := stripAnsiControlSequences(state.View()) + outputData[i] = txtar.File{ + Name: fmt.Sprintf("%v (%v = %v)", test.name, test.time.Format(time.RFC3339), test.time.Unix()), + Data: []byte(observed), + } + } + + archive := txtar.Archive{ + Comment: testData.Comment, + Files: outputData, + } + os.WriteFile(testDataFile, txtar.Format(&archive), 0666) + + for i, test := range tests { + observed := stripAnsiControlSequencesAndNewline(outputData[i].Data) + expected := stripAnsiControlSequencesAndNewline(testData.Files[i].Data) + if observed != expected { + t.Errorf("Midnight DST: Mismatched %s: Check git diff %s", test.name, testDataFile) + } + } +} + +func TestFractionalTimezoneOffsets(t *testing.T) { + testDataFile := "testdata/view/test-fractional-timezone-offsets.txt" + testData, err := txtar.ParseFile(testDataFile) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + datetime string + keystrokes string + }{ + {"Start time", "2017-11-05T00:29:02Z", ""}, + {"Plus 1 hour", "2017-11-05T01:29:02Z", "l"}, + {"Minus 15 minutes", "2017-11-05T01:14:02Z", "---------------"}, + {"Return to start", "2017-11-05T00:29:02Z", "HLpn+++++++++++++++h"}, + {"Minus 1 hour, date changed", "2017-11-04T23:29:02Z", "h"}, + {"Return to start", "2017-11-05T00:29:02Z", "l"}, + {"Zero minutes", "2017-11-05T00:00:00Z", "0"}, + } + + start, err := time.Parse(time.RFC3339, tests[0].datetime) + if err != nil { + t.Fatalf("Could not parse test Start time: %v", err) + } + + state := &model{ + zones: LoadDstTestZones(t), + clock: *NewClockTime(start), + keymaps: DefaultKeymaps, + isMilitary: true, + showDates: true, + } + + var observations []string + var outputData = []txtar.File{} + for _, test := range tests { + for k, key := range test.keystrokes { + msg := tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune{key}, + Alt: false, + } + + _, cmd := state.Update(msg) + if cmd != nil { + t.Fatalf("Expected nil Cmd for '%v' (key %v), but got %v", key, k, cmd) + } + } + + observed := stripAnsiControlSequences(state.View()) + observations = append(observations, observed) + observedDatetime := state.clock.t.Format(time.RFC3339) + observedUnixtime := state.clock.t.Unix() + if observedDatetime != test.datetime { + t.Errorf("Fraction Timezones: Mismatched datetime for %v: expected %v but got %v", test.name, test.datetime, observedDatetime) + } + outputData = append( + outputData, + txtar.File{ + Name: fmt.Sprintf("%v (%v = %v)", test.name, observedDatetime, observedUnixtime), + Data: []byte(observed), + }, + ) + } + + archive := txtar.Archive{ + Comment: testData.Comment, + Files: outputData, + } + os.WriteFile(testDataFile, txtar.Format(&archive), 0666) + + for i, test := range tests { + var expected string = "" + if len(testData.Files) > 0 { + expected = stripAnsiControlSequencesAndNewline(testData.Files[i].Data) + } + observed := stripAnsiControlSequencesAndNewline(outputData[i].Data) + if expected != observed { + t.Errorf("Fraction Timezones: Mismatched %s: Check git diff %s", test.name, testDataFile) + } + } +} + +func TestHighlightMarkers(t *testing.T) { + testDataFile := "testdata/view/test-highlight-markers.txt" + testData, err := txtar.ParseFile(testDataFile) + if err != nil { + t.Fatal(err) + } + + expected := stripAnsiControlSequencesAndNewline(testData.Files[0].Data) + + keys := "jkj" // down, up, down + + var state = utcMinuteAfterMidnightModel + for k, key := range keys { + msg := tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune{key}, + Alt: false, + } + _, cmd := state.Update(msg) + if cmd != nil { + t.Fatalf("Expected nil Cmd for '%v' (key %v), but got %v", key, k, cmd) + } + } + + observed := txtar.File{ + Name: "Highlight Local Zone", + Data: []byte(stripAnsiControlSequences(state.View())), + } + + archive := txtar.Archive{ + Comment: testData.Comment, + Files: []txtar.File{observed}, + } + os.WriteFile(testDataFile, txtar.Format(&archive), 0666) + + if expected != stripAnsiControlSequencesAndNewline(observed.Data) { + t.Errorf("Fraction Timezones: Mismatched highlight markers: Check git diff %s", testDataFile) + } +} + +// Test all vertical alignments, from the perspectives of different local zones +func TestLocalTimezones(t *testing.T) { + testDataFile := "testdata/view/test-local-timezones.txt" + testData, err := txtar.ParseFile(testDataFile) + if err != nil { + t.Fatal(err) + } + + testGroups := [][]struct { + localZone string + localTime string + }{ + { + {"UTC", "2017-11-05T00:00:00Z"}, + {"Asia/Calcutta", "2017-11-05T05:30:00+05:30"}, + {"Cuba", "2017-11-04T20:00:00-04:00"}, + }, + { + {"UTC", "2017-11-06T00:30:00Z"}, + {"Asia/Calcutta", "2017-11-06T06:00:00+05:30"}, + {"Cuba", "2017-11-05T19:30:00-05:00"}, + }, + } + + displayZones := LoadDstTestZones(t) + var outputData = []txtar.File{} + for i, testGroup := range testGroups { + for j, test := range testGroup { + testTime, err := time.Parse(time.RFC3339, test.localTime) + if err != nil { + t.Fatalf("Could not parse test time configuration [%v][%v]: '%v'", i, j, test.localTime) + } + + localZoneAtTop := make([]*Zone, 0, len(displayZones) + 1) + for _, zone := range(displayZones) { + if zone.DbName == test.localZone { + // Copy localZone to the top of list to render all other zones relative to it + localZone := *zone + localZone.Name = "Local" + localZoneAtTop = append(localZoneAtTop, &localZone) + localZoneAtTop = append(localZoneAtTop, displayZones...) + break + } + } + if len(localZoneAtTop) == 0 { + t.Fatalf("Could not find displayable timezone for case [%v][%v]: '%v'", i, j, test.localTime) + } + + state := model{ + zones: localZoneAtTop, + clock: *NewClockTime(testTime), + isMilitary: true, + showDates: true, + formatStyle: IsoFormatStyle, + zoneStyle: WithRelativeZoneStyle, + } + + observed := stripAnsiControlSequences(state.View()) + outputData = append(outputData, txtar.File{ + Name: fmt.Sprintf("[%v][%v] %v (%v = %v)", i, j, test.localZone, testTime.Format(time.RFC3339), testTime.Unix()), + Data: []byte(observed), + }) + } + } + + archive := txtar.Archive{ + Comment: testData.Comment, + Files: outputData, + } + os.WriteFile(testDataFile, txtar.Format(&archive), 0666) + + var count = 0 + for i, testGroup := range testGroups { + // Implementation explained in the testData file: + comparisonColumns := make([]string, len(testGroup)) + + for j, test := range testGroup { + // Test for any changes + observed := stripAnsiControlSequencesAndNewline(outputData[count].Data) + expected := stripAnsiControlSequencesAndNewline(testData.Files[count].Data) + if observed != expected { + t.Errorf("Local Timezones: Unexpected result [%v][%v] for %v: Check git diff %v", i, j, test.localZone, testDataFile) + } + + // Test for expected properties (this is sensitive to the layout format) + hourIndex := 11 + localTime := test.localTime[hourIndex:hourIndex + 2] + if localTime[0] == '0' { + localTime = " " + localTime[1:] + } + localTime = " " + localTime + " " + + localLine := 4 + lines := strings.Split(observed, "\n") + columnIndex := strings.Index(lines[localLine], localTime) + if columnIndex < 0 { + t.Errorf("Local Timezones: Failed [%v][%v] for %v: Could not find local hour %v", i, j, test.localZone, localTime) + } else { + rowGap := 3 + column := strings.Builder{} + for l := localLine + rowGap; l < len(lines); l += rowGap { + line := lines[l] + column.WriteString(line[columnIndex:columnIndex + 3]) + } + comparisonColumns[j] = column.String() + } + + count = count + 1 + } + + for _, result := range comparisonColumns { + if result != comparisonColumns[0] { + t.Errorf("Local Timezones: Inconsistencies in group [%v]:\n%v", i, strings.Join(comparisonColumns, "\n")) + break + } + } + + } +} + func TestRightAlignment(t *testing.T) { testDataFile := "testdata/view/test-right-alignment.txt" testData, err := txtar.ParseFile(testDataFile) @@ -45,7 +447,7 @@ func TestRightAlignment(t *testing.T) { originalColumns := os.Getenv("COLUMNS") var observations []string for _, test := range tests { - os.Setenv("COLUMNS", strconv.Itoa(test.columns)) + os.Setenv("COLUMNS", fmt.Sprintf("%v", test.columns)) observed := stripAnsiControlSequences(utcMinuteAfterMidnightModel.View()) observations = append(observations, observed) } @@ -81,3 +483,168 @@ func TestRightAlignment(t *testing.T) { } } } + +func TestTimeFormats(t *testing.T) { + testDataFile := "testdata/view/test-time-formats.txt" + testData, err := txtar.ParseFile(testDataFile) + if err != nil { + t.Fatal(err) + } + + expected := stripAnsiControlSequencesAndNewline(testData.Files[0].Data) + + tests := []struct { + name string + formatStyle FormatStyle + }{ + {"DefaultFormatStyle", DefaultFormatStyle}, + {"IsoFormatStyle", IsoFormatStyle}, + {"UnixFormatStyle", UnixFormatStyle}, + } + + var observations []string + var outputs = []txtar.File{ + { + Name: "expected", + Data: []byte(expected), + }, + } + oldHasDarkBackground := hasDarkBackground + hasDarkBackground = true + var state = utcMinuteAfterMidnightModel + for i, test := range tests { + if state.formatStyle != test.formatStyle { + t.Errorf("Expected %s %v for test %d but got: %v", test.name, test.formatStyle, i, state.formatStyle) + } + observed := stripAnsiControlSequences(state.View()) + observations = append(observations, observed) + outputs = append( + outputs, + txtar.File{ + Name: fmt.Sprintf("observed: %v", test.name), + Data: []byte(observed), + }, + ) + msg := tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune{'f'}, + Alt: false, + } + _, cmd := state.Update(msg) + if cmd != nil { + t.Fatalf("Expected nil Cmd, but got %v", cmd) + } + } + hasDarkBackground = oldHasDarkBackground + + for i := len(tests) - 1; i >= 0; i-- { + msg := tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune{'F'}, + Alt: false, + } + _, cmd := state.Update(msg) + if cmd != nil { + t.Fatalf("Expected nil Cmd, but got %v", cmd) + } + if state.formatStyle != tests[i].formatStyle { + t.Errorf("Expected %s %v for reverse test %d but got: %v", tests[i].name, tests[i].formatStyle, i, state.formatStyle) + } + } + + archive := txtar.Archive{ + Comment: testData.Comment, + Files: outputs, + } + os.WriteFile(testDataFile, txtar.Format(&archive), 0666) + + expectations := strings.Split(expected, "\n") + for i, test := range tests { + if !strings.Contains(observations[i], expectations[i]) { + t.Errorf("Expected %v β€œ%s”, but got: β€œ%s”", test.name, expectations[i], observations[i]) + } + } +} + +func TestZoneStyles(t *testing.T) { + testDataFile := "testdata/view/test-zone-styles.txt" + testData, err := txtar.ParseFile(testDataFile) + if err != nil { + t.Fatal(err) + } + + expected := stripAnsiControlSequencesAndNewline(testData.Files[0].Data) + + tests := []struct { + name string + zoneStyle ZoneStyle + }{ + {"AbbreviationZoneStyle", AbbreviationZoneStyle}, + {"WithZOffsetZoneStyle", WithZOffsetZoneStyle}, + {"WithRelativeZoneStyle", WithRelativeZoneStyle}, + } + + var observations []string + var outputs = []txtar.File{ + { + Name: "expected", + Data: []byte(expected), + }, + } + oldHasDarkBackground := hasDarkBackground + hasDarkBackground = true + var state = utcMinuteAfterMidnightModel + state.isMilitary = false + for i, test := range tests { + if state.zoneStyle != test.zoneStyle { + t.Errorf("Expected %s %v for test %d but got: %v", test.name, test.zoneStyle, i, state.zoneStyle) + } + observed := stripAnsiControlSequences(state.View()) + observations = append(observations, observed) + outputs = append( + outputs, + txtar.File{ + Name: fmt.Sprintf("observed: %v", test.name), + Data: []byte(observed), + }, + ) + msg := tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune{'z'}, + Alt: false, + } + _, cmd := state.Update(msg) + if cmd != nil { + t.Fatalf("Expected nil Cmd, but got %v", cmd) + } + } + hasDarkBackground = oldHasDarkBackground + + for i := len(tests) - 1; i >= 0; i-- { + msg := tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune{'Z'}, + Alt: false, + } + _, cmd := state.Update(msg) + if cmd != nil { + t.Fatalf("Expected nil Cmd, but got %v", cmd) + } + if state.zoneStyle != tests[i].zoneStyle { + t.Errorf("Expected %s %v for reverse test %d but got: %v", tests[i].name, tests[i].zoneStyle, i, state.zoneStyle) + } + } + + archive := txtar.Archive{ + Comment: testData.Comment, + Files: outputs, + } + os.WriteFile(testDataFile, txtar.Format(&archive), 0666) + + expectations := strings.Split(expected, "\n") + for i, test := range tests { + if !strings.Contains(observations[i], expectations[i]) { + t.Errorf("Expected %v β€œ%s”, but got: β€œ%s”", test.name, expectations[i], observations[i]) + } + } +} diff --git a/zone.go b/zone.go index 32ddac6..693c35b 100644 --- a/zone.go +++ b/zone.go @@ -16,15 +16,21 @@ **/ package main -import "time" +import ( + "fmt" + "time" +) -var name, _ = time.Now().Zone() +var now = time.Now() +var name, _ = now.Zone() var DefaultZones = []*Zone{ { + Loc: now.Location(), Name: "Local", DbName: name, }, { + Loc: time.UTC, Name: "UTC", DbName: "UTC", }, @@ -48,14 +54,26 @@ var EmojiClocks = map[int]string{ // Zone stores the name of a time zone type Zone struct { + Loc *time.Location DbName string // Name in tzdata - Name string // Short name + Name string // Preferred name (user-provided, or else DbName by default) } func (z Zone) String() string { return z.Name } +func (z Zone) VerboseString(t time.Time) string { + return fmt.Sprintf("(%s) %s", z.Abbreviation(t), z.Name) +} + +// Abbreviated short name for the zone (e.g. acronym "ABC" if available, or else a number like "-3"). +// It depends dynamically on the daylight saving policy in the zone at time `t`. +func (z Zone) Abbreviation(t time.Time) string { + shortName, _ := z.currentTime(t).Zone() + return shortName +} + // ClockEmoji returns the corresponding emoji clock for a given hour func (z Zone) ClockEmoji(t time.Time) string { h := ((z.currentTime(t).Hour() % 12) + 12) % 12 @@ -75,11 +93,7 @@ func (z Zone) ShortMT(t time.Time) string { func (z Zone) currentTime(t time.Time) time.Time { zName, _ := t.Zone() if z.DbName != zName { - loc, err := time.LoadLocation(z.DbName) - if err != nil { - return t - } - return t.In(loc) + return t.In(z.Loc) } return t }