Passed
Push — master ( df7d70...d7b12b )
by Serhii
01:18
created

timeago.strTimestampToTime   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
dl 0
loc 7
ccs 2
cts 2
cp 1
crap 2
rs 10
c 0
b 0
f 0
nop 1
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