264 lines
6.5 KiB
Go
264 lines
6.5 KiB
Go
// Copyright 2013 Simon HEGE. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//timeago allows the formatting of time in terms of fuzzy timestamps.
|
|
//For example:
|
|
// one minute ago
|
|
// 3 years ago
|
|
// in 2 minutes
|
|
package timeago
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
Day time.Duration = time.Hour * 24
|
|
Month time.Duration = Day * 30
|
|
Year time.Duration = Day * 365
|
|
)
|
|
|
|
type FormatPeriod struct {
|
|
D time.Duration
|
|
One string
|
|
Many string
|
|
}
|
|
|
|
//Config allows the customization of timeago.
|
|
//You may configure string items (language, plurals, ...) and
|
|
//maximum allowed duration value for fuzzy formatting.
|
|
type Config struct {
|
|
PastPrefix string
|
|
PastSuffix string
|
|
FuturePrefix string
|
|
FutureSuffix string
|
|
|
|
Periods []FormatPeriod
|
|
|
|
Zero string
|
|
Max time.Duration //Maximum duration for using the special formatting.
|
|
//DefaultLayout is used if delta is greater than the minimum of last period
|
|
//in Periods and Max. It is the desired representation of the date 2nd of
|
|
// January 2006.
|
|
DefaultLayout string
|
|
}
|
|
|
|
//Predefined english configuration
|
|
var English = Config{
|
|
PastPrefix: "",
|
|
PastSuffix: " ago",
|
|
FuturePrefix: "in ",
|
|
FutureSuffix: "",
|
|
|
|
Periods: []FormatPeriod{
|
|
FormatPeriod{time.Second, "about a second", "%d seconds"},
|
|
FormatPeriod{time.Minute, "about a minute", "%d minutes"},
|
|
FormatPeriod{time.Hour, "about an hour", "%d hours"},
|
|
FormatPeriod{Day, "one day", "%d days"},
|
|
FormatPeriod{Month, "one month", "%d months"},
|
|
FormatPeriod{Year, "one year", "%d years"},
|
|
},
|
|
|
|
Zero: "about a second",
|
|
|
|
Max: 73 * time.Hour,
|
|
DefaultLayout: "2006-01-02",
|
|
}
|
|
|
|
var Portuguese = Config{
|
|
PastPrefix: "há ",
|
|
PastSuffix: "",
|
|
FuturePrefix: "daqui a ",
|
|
FutureSuffix: "",
|
|
|
|
Periods: []FormatPeriod{
|
|
FormatPeriod{time.Second, "um segundo", "%d segundos"},
|
|
FormatPeriod{time.Minute, "um minuto", "%d minutos"},
|
|
FormatPeriod{time.Hour, "uma hora", "%d horas"},
|
|
FormatPeriod{Day, "um dia", "%d dias"},
|
|
FormatPeriod{Month, "um mês", "%d meses"},
|
|
FormatPeriod{Year, "um ano", "%d anos"},
|
|
},
|
|
|
|
Zero: "menos de um segundo",
|
|
|
|
Max: 73 * time.Hour,
|
|
DefaultLayout: "02-01-2006",
|
|
}
|
|
|
|
var Chinese = Config{
|
|
PastPrefix: "",
|
|
PastSuffix: "前",
|
|
FuturePrefix: "于 ",
|
|
FutureSuffix: "",
|
|
|
|
Periods: []FormatPeriod{
|
|
FormatPeriod{time.Second, "1 秒", "%d 秒"},
|
|
FormatPeriod{time.Minute, "1 分钟", "%d 分钟"},
|
|
FormatPeriod{time.Hour, "1 小时", "%d 小时"},
|
|
FormatPeriod{Day, "1 天", "%d 天"},
|
|
FormatPeriod{Month, "1 月", "%d 月"},
|
|
FormatPeriod{Year, "1 年", "%d 年"},
|
|
},
|
|
|
|
Zero: "1 秒",
|
|
|
|
Max: 73 * time.Hour,
|
|
DefaultLayout: "2006-01-02",
|
|
}
|
|
|
|
//Predefined french configuration
|
|
var French = Config{
|
|
PastPrefix: "il y a ",
|
|
PastSuffix: "",
|
|
FuturePrefix: "dans ",
|
|
FutureSuffix: "",
|
|
|
|
Periods: []FormatPeriod{
|
|
FormatPeriod{time.Second, "environ une seconde", "moins d'une minute"},
|
|
FormatPeriod{time.Minute, "environ une minute", "%d minutes"},
|
|
FormatPeriod{time.Hour, "environ une heure", "%d heures"},
|
|
FormatPeriod{Day, "un jour", "%d jours"},
|
|
FormatPeriod{Month, "un mois", "%d mois"},
|
|
FormatPeriod{Year, "un an", "%d ans"},
|
|
},
|
|
|
|
Zero: "environ une seconde",
|
|
|
|
Max: 73 * time.Hour,
|
|
DefaultLayout: "02/01/2006",
|
|
}
|
|
|
|
//Predefined german configuration
|
|
var German = Config{
|
|
PastPrefix: "vor ",
|
|
PastSuffix: "",
|
|
FuturePrefix: "in ",
|
|
FutureSuffix: "",
|
|
|
|
Periods: []FormatPeriod{
|
|
FormatPeriod{time.Second, "einer Sekunde", "%d Sekunden"},
|
|
FormatPeriod{time.Minute, "einer Minute", "%d Minuten"},
|
|
FormatPeriod{time.Hour, "einer Stunde", "%d Stunden"},
|
|
FormatPeriod{Day, "einem Tag", "%d Tagen"},
|
|
FormatPeriod{Month, "einem Monat", "%d Monaten"},
|
|
FormatPeriod{Year, "einem Jahr", "%d Jahren"},
|
|
},
|
|
|
|
Zero: "einer Sekunde",
|
|
|
|
Max: 73 * time.Hour,
|
|
DefaultLayout: "02.01.2006",
|
|
}
|
|
|
|
//Format returns a textual representation of the time value formatted according to the layout
|
|
//defined in the Config. The time is compared to time.Now() and is then formatted as a fuzzy
|
|
//timestamp (eg. "4 days ago")
|
|
func (cfg Config) Format(t time.Time) string {
|
|
return cfg.FormatReference(t, time.Now())
|
|
}
|
|
|
|
//FormatReference is the same as Format, but the reference has to be defined by the caller
|
|
func (cfg Config) FormatReference(t time.Time, reference time.Time) string {
|
|
|
|
d := reference.Sub(t)
|
|
|
|
if (d >= 0 && d >= cfg.Max) || (d < 0 && -d >= cfg.Max) {
|
|
return t.Format(cfg.DefaultLayout)
|
|
}
|
|
|
|
return cfg.FormatRelativeDuration(d)
|
|
}
|
|
|
|
//FormatRelativeDuration is the same as Format, but for time.Duration.
|
|
//Config.Max is not used in this function, as there is no other alternative.
|
|
func (cfg Config) FormatRelativeDuration(d time.Duration) string {
|
|
|
|
isPast := d >= 0
|
|
|
|
if d < 0 {
|
|
d = -d
|
|
}
|
|
|
|
s, _ := cfg.getTimeText(d, true)
|
|
|
|
if isPast {
|
|
return strings.Join([]string{cfg.PastPrefix, s, cfg.PastSuffix}, "")
|
|
} else {
|
|
return strings.Join([]string{cfg.FuturePrefix, s, cfg.FutureSuffix}, "")
|
|
}
|
|
}
|
|
|
|
//Round the duration d in terms of step.
|
|
func round(d time.Duration, step time.Duration, roundCloser bool) time.Duration {
|
|
|
|
if roundCloser {
|
|
return time.Duration(float64(d)/float64(step) + 0.5)
|
|
}
|
|
|
|
return time.Duration(float64(d) / float64(step))
|
|
}
|
|
|
|
//Count the number of parameters in a format string
|
|
func nbParamInFormat(f string) int {
|
|
return strings.Count(f, "%") - 2*strings.Count(f, "%%")
|
|
}
|
|
|
|
//Convert a duration to a text, based on the current config
|
|
func (cfg Config) getTimeText(d time.Duration, roundCloser bool) (string, time.Duration) {
|
|
if len(cfg.Periods) == 0 || d < cfg.Periods[0].D {
|
|
return cfg.Zero, 0
|
|
}
|
|
|
|
for i, p := range cfg.Periods {
|
|
|
|
next := p.D
|
|
if i+1 < len(cfg.Periods) {
|
|
next = cfg.Periods[i+1].D
|
|
}
|
|
|
|
if i+1 == len(cfg.Periods) || d < next {
|
|
|
|
r := round(d, p.D, roundCloser)
|
|
|
|
if next != p.D && r == round(next, p.D, roundCloser) {
|
|
continue
|
|
}
|
|
|
|
if r == 0 {
|
|
return "", d
|
|
}
|
|
|
|
layout := p.Many
|
|
if r == 1 {
|
|
layout = p.One
|
|
}
|
|
|
|
if nbParamInFormat(layout) == 0 {
|
|
return layout, d - r*p.D
|
|
}
|
|
|
|
return fmt.Sprintf(layout, r), d - r*p.D
|
|
}
|
|
}
|
|
|
|
return d.String(), 0
|
|
}
|
|
|
|
//NoMax creates an new config without a maximum
|
|
func NoMax(cfg Config) Config {
|
|
return WithMax(cfg, 9223372036854775807, time.RFC3339)
|
|
}
|
|
|
|
//WithMax creates an new config with special formatting limited to durations less than max.
|
|
//Values greater than max will be formatted by the standard time package using the defaultLayout.
|
|
func WithMax(cfg Config, max time.Duration, defaultLayout string) Config {
|
|
n := cfg
|
|
n.Max = max
|
|
n.DefaultLayout = defaultLayout
|
|
return n
|
|
}
|