diff options
Diffstat (limited to 'modules/timeutil')
-rw-r--r-- | modules/timeutil/datetime.go | 68 | ||||
-rw-r--r-- | modules/timeutil/datetime_test.go | 47 | ||||
-rw-r--r-- | modules/timeutil/executable.go | 50 | ||||
-rw-r--r-- | modules/timeutil/since.go | 145 | ||||
-rw-r--r-- | modules/timeutil/since_test.go | 87 | ||||
-rw-r--r-- | modules/timeutil/timestamp.go | 100 | ||||
-rw-r--r-- | modules/timeutil/timestampnano.go | 28 |
7 files changed, 525 insertions, 0 deletions
diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go new file mode 100644 index 00000000..c0891735 --- /dev/null +++ b/modules/timeutil/datetime.go @@ -0,0 +1,68 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "fmt" + "html" + "html/template" + "strings" + "time" +) + +// DateTime renders an absolute time HTML element by datetime. +func DateTime(format string, datetime any, extraAttrs ...string) template.HTML { + // TODO: remove the extraAttrs argument, it's not used in any call to DateTime + + if p, ok := datetime.(*time.Time); ok { + datetime = *p + } + if p, ok := datetime.(*TimeStamp); ok { + datetime = *p + } + switch v := datetime.(type) { + case TimeStamp: + datetime = v.AsTime() + case int: + datetime = TimeStamp(v).AsTime() + case int64: + datetime = TimeStamp(v).AsTime() + } + + var datetimeEscaped, textEscaped string + switch v := datetime.(type) { + case nil: + return "-" + case string: + datetimeEscaped = html.EscapeString(v) + textEscaped = datetimeEscaped + case time.Time: + if v.IsZero() || v.Unix() == 0 { + return "-" + } + datetimeEscaped = html.EscapeString(v.Format(time.RFC3339)) + if format == "full" { + textEscaped = html.EscapeString(v.Format("2006-01-02 15:04:05 -07:00")) + } else { + textEscaped = html.EscapeString(v.Format("2006-01-02")) + } + default: + panic(fmt.Sprintf("Unsupported time type %T", datetime)) + } + + attrs := make([]string, 0, 10+len(extraAttrs)) + attrs = append(attrs, extraAttrs...) + attrs = append(attrs, `weekday=""`, `year="numeric"`) + + switch format { + case "short", "long": // date only + attrs = append(attrs, `month="`+format+`"`, `day="numeric"`) + return template.HTML(fmt.Sprintf(`<absolute-date %s date="%s">%s</absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped)) + case "full": // full date including time + attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`) + return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped)) + default: + panic(fmt.Sprintf("Unsupported format %s", format)) + } +} diff --git a/modules/timeutil/datetime_test.go b/modules/timeutil/datetime_test.go new file mode 100644 index 00000000..ac2ce35b --- /dev/null +++ b/modules/timeutil/datetime_test.go @@ -0,0 +1,47 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "testing" + "time" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestDateTime(t *testing.T) { + testTz, _ := time.LoadLocation("America/New_York") + defer test.MockVariableValue(&setting.DefaultUILocation, testTz)() + + refTimeStr := "2018-01-01T00:00:00Z" + refDateStr := "2018-01-01" + refTime, _ := time.Parse(time.RFC3339, refTimeStr) + refTimeStamp := TimeStamp(refTime.Unix()) + + assert.EqualValues(t, "-", DateTime("short", nil)) + assert.EqualValues(t, "-", DateTime("short", 0)) + assert.EqualValues(t, "-", DateTime("short", time.Time{})) + assert.EqualValues(t, "-", DateTime("short", TimeStamp(0))) + + actual := DateTime("short", "invalid") + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="invalid">invalid</absolute-date>`, actual) + + actual = DateTime("short", refTimeStr) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</absolute-date>`, actual) + + actual = DateTime("short", refTime) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01</absolute-date>`, actual) + + actual = DateTime("short", refDateStr) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01">2018-01-01</absolute-date>`, actual) + + actual = DateTime("short", refTimeStamp) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2017-12-31T19:00:00-05:00">2017-12-31</absolute-date>`, actual) + + actual = DateTime("full", refTimeStamp) + assert.EqualValues(t, `<relative-time weekday="" year="numeric" format="datetime" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" data-tooltip-content data-tooltip-interactive="true" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual) +} diff --git a/modules/timeutil/executable.go b/modules/timeutil/executable.go new file mode 100644 index 00000000..57ae8b2a --- /dev/null +++ b/modules/timeutil/executable.go @@ -0,0 +1,50 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "os" + "path/filepath" + "sync" + "time" + + "code.gitea.io/gitea/modules/log" +) + +var ( + executablModTime = time.Now() + executablModTimeOnce sync.Once +) + +// GetExecutableModTime get executable file modified time of current process. +func GetExecutableModTime() time.Time { + executablModTimeOnce.Do(func() { + exePath, err := os.Executable() + if err != nil { + log.Error("os.Executable: %v", err) + return + } + + exePath, err = filepath.Abs(exePath) + if err != nil { + log.Error("filepath.Abs: %v", err) + return + } + + exePath, err = filepath.EvalSymlinks(exePath) + if err != nil { + log.Error("filepath.EvalSymlinks: %v", err) + return + } + + st, err := os.Stat(exePath) + if err != nil { + log.Error("os.Stat: %v", err) + return + } + + executablModTime = st.ModTime() + }) + return executablModTime +} diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go new file mode 100644 index 00000000..dba42c79 --- /dev/null +++ b/modules/timeutil/since.go @@ -0,0 +1,145 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "fmt" + "html/template" + "strings" + "time" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" +) + +// Seconds-based time units +const ( + Minute = 60 + Hour = 60 * Minute + Day = 24 * Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month +) + +func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) { + var diffStr string + switch { + case diff <= 0: + diff = 0 + diffStr = lang.TrString("tool.now") + case diff < 2: + diff = 0 + diffStr = lang.TrString("tool.1s") + case diff < 1*Minute: + diffStr = lang.TrString("tool.seconds", diff) + diff = 0 + + case diff < 2*Minute: + diff -= 1 * Minute + diffStr = lang.TrString("tool.1m") + case diff < 1*Hour: + diffStr = lang.TrString("tool.minutes", diff/Minute) + diff -= diff / Minute * Minute + + case diff < 2*Hour: + diff -= 1 * Hour + diffStr = lang.TrString("tool.1h") + case diff < 1*Day: + diffStr = lang.TrString("tool.hours", diff/Hour) + diff -= diff / Hour * Hour + + case diff < 2*Day: + diff -= 1 * Day + diffStr = lang.TrString("tool.1d") + case diff < 1*Week: + diffStr = lang.TrString("tool.days", diff/Day) + diff -= diff / Day * Day + + case diff < 2*Week: + diff -= 1 * Week + diffStr = lang.TrString("tool.1w") + case diff < 1*Month: + diffStr = lang.TrString("tool.weeks", diff/Week) + diff -= diff / Week * Week + + case diff < 2*Month: + diff -= 1 * Month + diffStr = lang.TrString("tool.1mon") + case diff < 1*Year: + diffStr = lang.TrString("tool.months", diff/Month) + diff -= diff / Month * Month + + case diff < 2*Year: + diff -= 1 * Year + diffStr = lang.TrString("tool.1y") + default: + diffStr = lang.TrString("tool.years", diff/Year) + diff -= (diff / Year) * Year + } + return diff, diffStr +} + +// MinutesToFriendly returns a user friendly string with number of minutes +// converted to hours and minutes. +func MinutesToFriendly(minutes int, lang translation.Locale) string { + duration := time.Duration(minutes) * time.Minute + return TimeSincePro(time.Now().Add(-duration), lang) +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSincePro(then time.Time, lang translation.Locale) string { + return timeSincePro(then, time.Now(), lang) +} + +func timeSincePro(then, now time.Time, lang translation.Locale) string { + diff := now.Unix() - then.Unix() + + if then.After(now) { + return lang.TrString("tool.future") + } + if diff == 0 { + return lang.TrString("tool.now") + } + + var timeStr, diffStr string + for { + if diff == 0 { + break + } + + diff, diffStr = computeTimeDiffFloor(diff, lang) + timeStr += ", " + diffStr + } + return strings.TrimPrefix(timeStr, ", ") +} + +func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML { + friendlyText := then.Format("2006-01-02 15:04:05 -07:00") + + // document: https://github.com/github/relative-time-element + attrs := `tense="past"` + isFuture := now.Before(then) + if isFuture { + attrs = `tense="future"` + } + + // declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip + htm := fmt.Sprintf(`<relative-time prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`, + attrs, then.Format(time.RFC3339), friendlyText) + return template.HTML(htm) +} + +// TimeSince renders relative time HTML given a time.Time +func TimeSince(then time.Time, lang translation.Locale) template.HTML { + if setting.UI.PreferredTimestampTense == "absolute" { + return DateTime("full", then) + } + return timeSinceUnix(then, time.Now(), lang) +} + +// TimeSinceUnix renders relative time HTML given a TimeStamp +func TimeSinceUnix(then TimeStamp, lang translation.Locale) template.HTML { + return TimeSince(then.AsLocalTime(), lang) +} diff --git a/modules/timeutil/since_test.go b/modules/timeutil/since_test.go new file mode 100644 index 00000000..40fefe87 --- /dev/null +++ b/modules/timeutil/since_test.go @@ -0,0 +1,87 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "context" + "os" + "testing" + "time" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" + + "github.com/stretchr/testify/assert" +) + +var BaseDate time.Time + +// time durations +const ( + DayDur = 24 * time.Hour + WeekDur = 7 * DayDur + MonthDur = 30 * DayDur + YearDur = 12 * MonthDur +) + +func TestMain(m *testing.M) { + setting.StaticRootPath = "../../" + setting.Names = []string{"english"} + setting.Langs = []string{"en-US"} + // setup + translation.InitLocales(context.Background()) + BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) + + // run the tests + retVal := m.Run() + + os.Exit(retVal) +} + +func TestTimeSincePro(t *testing.T) { + assert.Equal(t, "now", timeSincePro(BaseDate, BaseDate, translation.NewLocale("en-US"))) + + // test that a difference of `diff` yields the expected string + test := func(expected string, diff time.Duration) { + actual := timeSincePro(BaseDate, BaseDate.Add(diff), translation.NewLocale("en-US")) + assert.Equal(t, expected, actual) + assert.Equal(t, "future", timeSincePro(BaseDate.Add(diff), BaseDate, translation.NewLocale("en-US"))) + } + test("1 second", time.Second) + test("2 seconds", 2*time.Second) + test("1 minute", time.Minute) + test("1 minute, 1 second", time.Minute+time.Second) + test("1 minute, 59 seconds", time.Minute+59*time.Second) + test("2 minutes", 2*time.Minute) + test("1 hour", time.Hour) + test("1 hour, 1 second", time.Hour+time.Second) + test("1 hour, 59 minutes, 59 seconds", time.Hour+59*time.Minute+59*time.Second) + test("2 hours", 2*time.Hour) + test("1 day", DayDur) + test("1 day, 23 hours, 59 minutes, 59 seconds", + DayDur+23*time.Hour+59*time.Minute+59*time.Second) + test("2 days", 2*DayDur) + test("1 week", WeekDur) + test("2 weeks", 2*WeekDur) + test("1 month", MonthDur) + test("3 months", 3*MonthDur) + test("1 year", YearDur) + test("2 years, 3 months, 1 week, 2 days, 4 hours, 12 minutes, 17 seconds", + 2*YearDur+3*MonthDur+WeekDur+2*DayDur+4*time.Hour+ + 12*time.Minute+17*time.Second) +} + +func TestMinutesToFriendly(t *testing.T) { + // test that a number of minutes yields the expected string + test := func(expected string, minutes int) { + actual := MinutesToFriendly(minutes, translation.NewLocale("en-US")) + assert.Equal(t, expected, actual) + } + test("1 minute", 1) + test("2 minutes", 2) + test("1 hour", 60) + test("1 hour, 1 minute", 61) + test("1 hour, 2 minutes", 62) + test("2 hours", 120) +} diff --git a/modules/timeutil/timestamp.go b/modules/timeutil/timestamp.go new file mode 100644 index 00000000..27a80b66 --- /dev/null +++ b/modules/timeutil/timestamp.go @@ -0,0 +1,100 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "time" + + "code.gitea.io/gitea/modules/setting" +) + +// TimeStamp defines a timestamp +type TimeStamp int64 + +var ( + // mockNow is NOT concurrency-safe!! + mockNow time.Time + + // Used for IsZero, to check if timestamp is the zero time instant. + timeZeroUnix = time.Time{}.Unix() +) + +// MockSet sets the time to a mocked time.Time +func MockSet(now time.Time) { + mockNow = now +} + +// MockUnset will unset the mocked time.Time +func MockUnset() { + mockNow = time.Time{} +} + +// TimeStampNow returns now int64 +func TimeStampNow() TimeStamp { + if !mockNow.IsZero() { + return TimeStamp(mockNow.Unix()) + } + return TimeStamp(time.Now().Unix()) +} + +// Add adds seconds and return sum +func (ts TimeStamp) Add(seconds int64) TimeStamp { + return ts + TimeStamp(seconds) +} + +// AddDuration adds time.Duration and return sum +func (ts TimeStamp) AddDuration(interval time.Duration) TimeStamp { + return ts + TimeStamp(interval/time.Second) +} + +// Year returns the time's year +func (ts TimeStamp) Year() int { + return ts.AsTime().Year() +} + +// AsTime convert timestamp as time.Time in Local locale +func (ts TimeStamp) AsTime() (tm time.Time) { + return ts.AsTimeInLocation(setting.DefaultUILocation) +} + +// AsLocalTime convert timestamp as time.Time in local location +func (ts TimeStamp) AsLocalTime() time.Time { + return time.Unix(int64(ts), 0) +} + +// AsTimeInLocation convert timestamp as time.Time in Local locale +func (ts TimeStamp) AsTimeInLocation(loc *time.Location) time.Time { + return time.Unix(int64(ts), 0).In(loc) +} + +// AsTimePtr convert timestamp as *time.Time in Local locale +func (ts TimeStamp) AsTimePtr() *time.Time { + return ts.AsTimePtrInLocation(setting.DefaultUILocation) +} + +// AsTimePtrInLocation convert timestamp as *time.Time in customize location +func (ts TimeStamp) AsTimePtrInLocation(loc *time.Location) *time.Time { + tm := time.Unix(int64(ts), 0).In(loc) + return &tm +} + +// Format formats timestamp as given format +func (ts TimeStamp) Format(f string) string { + return ts.FormatInLocation(f, setting.DefaultUILocation) +} + +// FormatInLocation formats timestamp as given format with spiecific location +func (ts TimeStamp) FormatInLocation(f string, loc *time.Location) string { + return ts.AsTimeInLocation(loc).Format(f) +} + +// FormatDate formats a date in YYYY-MM-DD +func (ts TimeStamp) FormatDate() string { + return ts.Format("2006-01-02") +} + +// IsZero is zero time +func (ts TimeStamp) IsZero() bool { + return int64(ts) == 0 || int64(ts) == timeZeroUnix +} diff --git a/modules/timeutil/timestampnano.go b/modules/timeutil/timestampnano.go new file mode 100644 index 00000000..4a9f7955 --- /dev/null +++ b/modules/timeutil/timestampnano.go @@ -0,0 +1,28 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "time" + + "code.gitea.io/gitea/modules/setting" +) + +// TimeStampNano is for nano time in database, do not use it unless there is a real requirement. +type TimeStampNano int64 + +// TimeStampNanoNow returns now nano int64 +func TimeStampNanoNow() TimeStampNano { + return TimeStampNano(time.Now().UnixNano()) +} + +// AsTime convert timestamp as time.Time in Local locale +func (tsn TimeStampNano) AsTime() (tm time.Time) { + return tsn.AsTimeInLocation(setting.DefaultUILocation) +} + +// AsTimeInLocation convert timestamp as time.Time in Local locale +func (tsn TimeStampNano) AsTimeInLocation(loc *time.Location) time.Time { + return time.Unix(0, int64(tsn)).In(loc) +} |