Passed
Push — v3.3.0 ( 1f57ca )
by Serhii
01:46
created

timeago.strTimestampToTime   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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