timeago.findLangForms   B
last analyzed

Complexity

Conditions 8

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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