1 | package timeago |
||
2 | |||
3 | import ( |
||
4 | "math" |
||
5 | "strconv" |
||
6 | "strings" |
||
7 | "time" |
||
8 | |||
9 | "github.com/SerhiiCho/timeago/v3/internal/utils" |
||
10 | ) |
||
11 | |||
12 | var ( |
||
13 | // cachedJsonRes saves parsed JSON translations to prevent |
||
14 | // parsing the same JSON file multiple times. |
||
15 | cachedJsonRes = map[string]*LangSet{} |
||
16 | |||
17 | // options is a list of options that modify the final output. |
||
18 | // Some options are noSuffix, upcoming, online, and justNow. |
||
19 | 1 | options = []opt{} |
|
20 | |||
21 | 1 | // conf is configuration provided by the user. |
|
22 | conf = defaultConfig() |
||
23 | 1 | ||
24 | // langSet is a pointer to the current language set that |
||
25 | // is currently being used. |
||
26 | langSet *LangSet |
||
27 | 1 | ) |
|
28 | |||
29 | type timeNumbers struct { |
||
30 | 1 | Seconds int |
|
31 | Minutes int |
||
32 | 1 | Hours int |
|
33 | Days int |
||
34 | Weeks int |
||
35 | Months int |
||
36 | Years int |
||
37 | } |
||
38 | |||
39 | // Parse coverts provided datetime into `x time ago` format. |
||
40 | // The first argument can different types of inputs: |
||
41 | // 1. Unix timestamp: int, int32, int64, unit, uint32, uint64, string |
||
42 | // 2. Type from Go time package: time.Time |
||
43 | // 3. Datetime string in format 'YYYY-MM-DD HH:MM:SS': string |
||
44 | func Parse(date interface{}, opts ...opt) (string, error) { |
||
45 | options = []opt{} |
||
46 | langSet = nil |
||
47 | |||
48 | var t time.Time |
||
49 | var err error |
||
50 | |||
51 | switch userDate := date.(type) { |
||
52 | case int: |
||
53 | 1 | t = unixToTime(userDate) |
|
54 | case int32: |
||
55 | 1 | t = unixToTime(int(userDate)) |
|
56 | case int64: |
||
57 | t = unixToTime(int(userDate)) |
||
58 | case uint: |
||
59 | 1 | t = unixToTime(int(userDate)) |
|
60 | case uint32: |
||
61 | 1 | t = unixToTime(int(userDate)) |
|
62 | 1 | case uint64: |
|
63 | 1 | t = unixToTime(int(userDate)) |
|
64 | case string: |
||
65 | if isUnsignedInteger(userDate) { |
||
66 | 1 | t, err = strTimestampToTime(userDate) |
|
67 | } else { |
||
68 | t, err = strToTime(userDate) |
||
69 | } |
||
70 | default: |
||
71 | t = date.(time.Time) |
||
72 | } |
||
73 | 1 | ||
74 | if err != nil { |
||
75 | return "", err |
||
76 | } |
||
77 | |||
78 | enableOptions(opts) |
||
79 | |||
80 | return computeTimeSince(t) |
||
81 | } |
||
82 | |||
83 | // Configure applies the given configuration to the timeago without |
||
84 | // overriding the previous configuration. It will only override the |
||
85 | // provided configuration. If you want to override the previous |
||
86 | // configurations, use Reconfigure function instead. |
||
87 | 1 | func Configure(c Config) { |
|
88 | if c.OnlineThreshold > 0 { |
||
89 | 1 | conf.OnlineThreshold = c.OnlineThreshold |
|
90 | } |
||
91 | |||
92 | if c.JustNowThreshold > 0 { |
||
93 | conf.JustNowThreshold = c.JustNowThreshold |
||
94 | } |
||
95 | |||
96 | if c.Language != "" { |
||
97 | 1 | conf.Language = c.Language |
|
98 | } |
||
99 | 1 | ||
100 | if c.Location != "" { |
||
101 | 1 | conf.Location = c.Location |
|
102 | } |
||
103 | |||
104 | if len(c.Translations) > 0 { |
||
105 | conf.Translations = c.Translations |
||
106 | } |
||
107 | } |
||
108 | |||
109 | // Reconfigure reconfigures the timeago with the provided configuration. |
||
110 | // It will override the previous configuration with the new one. |
||
111 | func Reconfigure(c Config) { |
||
112 | conf = defaultConfig() |
||
113 | cachedJsonRes = map[string]*LangSet{} |
||
114 | Configure(c) |
||
115 | } |
||
116 | 1 | ||
117 | 1 | func defaultConfig() *Config { |
|
118 | 1 | return NewConfig("en", "UTC", []LangSet{}, 60, 60) |
|
119 | 1 | } |
|
120 | 1 | ||
121 | 1 | func strTimestampToTime(userDate string) (time.Time, error) { |
|
122 | sec, err := strconv.Atoi(userDate) |
||
123 | 1 | if err != nil { |
|
124 | return time.Time{}, err |
||
125 | } |
||
126 | |||
127 | return unixToTime(sec), nil |
||
128 | } |
||
129 | |||
130 | 1 | func strToTime(userDate string) (time.Time, error) { |
|
131 | 1 | if !conf.isLocationProvided() { |
|
132 | parsedTime, _ := time.Parse(time.DateTime, userDate) |
||
133 | 1 | return parsedTime, nil |
|
134 | 1 | } |
|
135 | |||
136 | 1 | loc, err := location() |
|
137 | 1 | if err != nil { |
|
138 | return time.Time{}, err |
||
139 | } |
||
140 | 1 | ||
141 | parsedTime, err := time.ParseInLocation(time.DateTime, userDate, loc) |
||
142 | if err != nil { |
||
143 | return time.Time{}, utils.Errorf("%v", err) |
||
144 | 1 | } |
|
145 | 1 | ||
146 | 1 | return parsedTime, nil |
|
147 | } |
||
148 | |||
149 | // location loads location from the time package |
||
150 | 1 | func location() (*time.Location, error) { |
|
151 | if !conf.isLocationProvided() { |
||
152 | return time.Now().Location(), nil |
||
153 | } |
||
154 | 1 | ||
155 | loc, err := time.LoadLocation(conf.Location) |
||
156 | if err != nil { |
||
157 | return nil, utils.Errorf("%v", err) |
||
158 | } |
||
159 | |||
160 | return loc, nil |
||
161 | } |
||
162 | |||
163 | func computeTimeSince(t time.Time) (string, error) { |
||
164 | now := time.Now() |
||
165 | var err error |
||
166 | |||
167 | // Adjust times based on location if provided |
||
168 | if t, now, err = adjustTimesForLocation(t, now); err != nil { |
||
169 | return "", err |
||
170 | } |
||
171 | |||
172 | timeInSec := computeTimeDifference(t, now) |
||
173 | |||
174 | if langSet, err = newLangSet(); err != nil { |
||
175 | return "", err |
||
176 | } |
||
177 | |||
178 | if optionIsEnabled(OptOnline) && timeInSec < conf.OnlineThreshold { |
||
179 | return langSet.Online, nil |
||
180 | } |
||
181 | |||
182 | if optionIsEnabled(OptJustNow) && timeInSec < conf.JustNowThreshold { |
||
183 | return langSet.JustNow, nil |
||
184 | } |
||
185 | |||
186 | var timeUnit string |
||
187 | |||
188 | langForms, timeNum := findLangForms(timeInSec) |
||
189 | |||
190 | if timeUnit, err = computeTimeUnit(langForms, timeNum); err != nil { |
||
191 | return "", err |
||
192 | } |
||
193 | |||
194 | suffix := computeSuffix() |
||
195 | |||
196 | return mergeFinalOutput(timeNum, timeUnit, suffix) |
||
197 | } |
||
198 | |||
199 | // adjustTimesForLocation adjusts the given times based on the provided location. |
||
200 | func adjustTimesForLocation(t, now time.Time) (time.Time, time.Time, error) { |
||
201 | if !conf.isLocationProvided() { |
||
202 | return t, now, nil |
||
203 | } |
||
204 | |||
205 | loc, err := location() |
||
206 | if err != nil { |
||
207 | return t, now, err |
||
208 | } |
||
209 | |||
210 | return t.In(loc), now.In(loc), nil |
||
211 | } |
||
212 | |||
213 | // computeTimeDifference returns the absolute time difference in seconds. |
||
214 | func computeTimeDifference(t, now time.Time) int { |
||
215 | timeInSec := int(now.Sub(t).Seconds()) |
||
216 | if timeInSec < 0 { |
||
217 | enableOption(OptUpcoming) |
||
218 | return -timeInSec |
||
219 | } |
||
220 | |||
221 | return timeInSec |
||
222 | } |
||
223 | |||
224 | func mergeFinalOutput(timeNum int, timeUnit, suffix string) (string, error) { |
||
225 | replacer := strings.NewReplacer( |
||
226 | "{timeUnit}", timeUnit, |
||
227 | "{num}", strconv.Itoa(timeNum), |
||
228 | "{ago}", suffix, |
||
229 | ) |
||
230 | |||
231 | out := replacer.Replace(langSet.Format) |
||
232 | |||
233 | return strings.TrimSpace(out), nil |
||
234 | } |
||
235 | |||
236 | func findLangForms(timeInSec int) (LangForms, int) { |
||
237 | nums := calculateTimeNumbers(float64(timeInSec)) |
||
238 | |||
239 | switch { |
||
240 | case timeInSec < 60: |
||
241 | return langSet.Second, nums.Seconds |
||
242 | case nums.Minutes < 60: |
||
243 | return langSet.Minute, nums.Minutes |
||
244 | case nums.Hours < 24: |
||
245 | return langSet.Hour, nums.Hours |
||
246 | case nums.Days < 7: |
||
247 | return langSet.Day, nums.Days |
||
248 | case nums.Weeks < 4: |
||
249 | return langSet.Week, nums.Weeks |
||
250 | case nums.Months < 12: |
||
251 | if nums.Months == 0 { |
||
252 | nums.Months = 1 |
||
253 | } |
||
254 | |||
255 | return langSet.Month, nums.Months |
||
256 | } |
||
257 | |||
258 | return langSet.Year, nums.Years |
||
259 | } |
||
260 | |||
261 | func computeSuffix() string { |
||
262 | if optionIsEnabled(OptNoSuffix) || optionIsEnabled(OptUpcoming) { |
||
263 | return "" |
||
264 | } |
||
265 | |||
266 | return langSet.Ago |
||
267 | } |
||
268 | |||
269 | func calculateTimeNumbers(seconds float64) timeNumbers { |
||
270 | minutes := math.Round(seconds / 60) |
||
271 | hours := math.Round(seconds / 3600) |
||
272 | days := math.Round(seconds / 86400) |
||
273 | weeks := math.Round(seconds / 604800) |
||
274 | months := math.Round(seconds / 2629440) |
||
275 | years := math.Round(seconds / 31553280) |
||
276 | |||
277 | return timeNumbers{ |
||
278 | Seconds: int(seconds), |
||
279 | Minutes: int(minutes), |
||
280 | Hours: int(hours), |
||
281 | Days: int(days), |
||
282 | Weeks: int(weeks), |
||
283 | Months: int(months), |
||
284 | Years: int(years), |
||
285 | } |
||
286 | } |
||
287 | |||
288 | func computeTimeUnit(langForm LangForms, num int) (string, error) { |
||
289 | form, err := timeUnitForm(num) |
||
290 | if err != nil { |
||
291 | return "", err |
||
292 | } |
||
293 | |||
294 | if unit, ok := langForm[form]; ok { |
||
295 | return unit, nil |
||
296 | } |
||
297 | |||
298 | return langForm["other"], nil |
||
299 | } |
||
300 | |||
301 | func timeUnitForm(num int) (string, error) { |
||
302 | rule, err := identifyGrammarRules(num, conf.Language) |
||
303 | if err != nil { |
||
304 | return "", err |
||
305 | } |
||
306 | |||
307 | switch { |
||
308 | case rule.Zero: |
||
309 | return "zero", nil |
||
310 | case rule.One: |
||
311 | return "one", nil |
||
312 | case rule.Few: |
||
313 | return "few", nil |
||
314 | case rule.Two: |
||
315 | return "two", nil |
||
316 | case rule.Many: |
||
317 | return "many", nil |
||
318 | } |
||
319 | |||
320 | return "other", nil |
||
321 | } |
||
322 |