github.com/viant/toolbox@v0.34.5/time_helper.go (about) 1 package toolbox 2 3 import ( 4 "fmt" 5 "log" 6 "strings" 7 "time" 8 ) 9 10 const ( 11 Now = "now" 12 Tomorrow = "tomorrow" 13 Yesterday = "yesterday" 14 15 //TimeAtTwoHoursAgo = "2hoursAgo" 16 //TimeAtHourAhead = "hourAhead" 17 //TimeAtTwoHoursAhead = "2hoursAhead" 18 19 DurationWeek = "week" 20 DurationDay = "day" 21 DurationHour = "hour" 22 DurationMinute = "minute" 23 DurationMinuteAbbr = "min" 24 DurationSecond = "second" 25 DurationSecondAbbr = "sec" 26 DurationMillisecond = "millisecond" 27 DurationMillisecondAbbr = "ms" 28 DurationMicrosecond = "microsecond" 29 DurationMicrosecondAbbr = "us" 30 DurationNanosecond = "nanosecond" 31 DurationNanosecondAbbr = "ns" 32 ) 33 34 //AtTime represents a time at given schedule 35 type AtTime struct { 36 WeekDay string 37 Hour string 38 Minute string 39 TZ string 40 loc *time.Location 41 } 42 43 func (t *AtTime) min(base time.Time) int { 44 switch t.Minute { 45 case "*": 46 return (base.Minute() + 1) % 59 47 case "": 48 return 0 49 } 50 candidates := strings.Split(t.Minute, ",") 51 for _, candidate := range candidates { 52 candidateMin := AsInt(candidate) 53 if base.Minute() < candidateMin { 54 return candidateMin 55 } 56 } 57 return AsInt(candidates[0]) 58 } 59 60 func (t *AtTime) hour(base time.Time) int { 61 min := t.min(base) 62 switch t.Hour { 63 case "*": 64 if min > base.Minute() { 65 return base.Hour() 66 } 67 return (base.Hour() + 1) % 23 68 case "": 69 return 0 70 } 71 candidates := strings.Split(t.Hour, ",") 72 for _, candidate := range candidates { 73 candidateHour := AsInt(candidate) 74 if base.Hour() < candidateHour { 75 return candidateHour 76 } 77 } 78 return AsInt(candidates[0]) 79 } 80 81 func (t *AtTime) weekday(base time.Time) int { 82 hour := t.hour(base) 83 min := t.min(base) 84 baseWeekday := int(base.Weekday()) 85 isPastDue := hour > base.Hour() || (hour == base.Hour() && min > base.Minute()) 86 switch t.WeekDay { 87 case "*": 88 if isPastDue { 89 return baseWeekday 90 } 91 return (baseWeekday + 1) % 7 92 case "": 93 return 0 94 } 95 candidates := strings.Split(t.WeekDay, ",") 96 result := AsInt(candidates[0]) 97 for _, candidate := range candidates { 98 candidateWeekday := AsInt(candidate) 99 if baseWeekday < candidateWeekday { 100 result = candidateWeekday 101 break 102 } 103 } 104 if result < baseWeekday && isPastDue { 105 return 7 + result 106 } 107 108 return result 109 } 110 111 //Init initializes tz 112 func (t *AtTime) Init() error { 113 if t.TZ == "" { 114 return nil 115 } 116 var err error 117 t.loc, err = time.LoadLocation(t.TZ) 118 return err 119 } 120 121 //Next returns next time schedule 122 func (t *AtTime) Next(base time.Time) time.Time { 123 124 if t.loc != nil && base.Location() != nil && base.Location() != t.loc { 125 base = base.In(t.loc) 126 } else { 127 t.loc = base.Location() 128 } 129 130 min := t.min(base) 131 hour := t.hour(base) 132 timeLiteral := base.Format("2006-01-02") 133 updateTimeLiteral := fmt.Sprintf("%v %02d:%02d:00", timeLiteral, hour, min) 134 weekday := t.weekday(base) 135 baseWeekday := int(base.Weekday()) 136 weekdayDiff := 0 137 if weekday >= baseWeekday { 138 weekdayDiff = weekday - baseWeekday 139 } else { 140 weekdayDiff = 7 + weekday - baseWeekday 141 } 142 143 var result time.Time 144 if t.loc != nil { 145 result, _ = time.ParseInLocation("2006-01-02 15:04:05", updateTimeLiteral, t.loc) 146 } else { 147 result, _ = time.Parse("2006-01-02 15:04:05", updateTimeLiteral) 148 } 149 150 if weekdayDiff > 0 { 151 result = result.Add(time.Hour * 24 * time.Duration(weekdayDiff)) 152 } else if weekdayDiff == 0 && AsInt(t.WeekDay) > 0 { 153 result = result.Add(time.Hour * 24 * 7) 154 } 155 156 if result.UnixNano() < base.UnixNano() { 157 log.Printf("invalid schedule next: %v is before base: %v\n", result, base) 158 } 159 return result 160 } 161 162 //Duration represents duration 163 type Duration struct { 164 Value int 165 Unit string 166 } 167 168 //Duration return durations 169 func (d Duration) Duration() (time.Duration, error) { 170 return NewDuration(d.Value, d.Unit) 171 } 172 173 //NewDuration returns a durationToken for supplied value and time unit, 3, "second" 174 func NewDuration(value int, unit string) (time.Duration, error) { 175 var duration time.Duration 176 switch unit { 177 case DurationWeek: 178 duration = time.Hour * 24 * 7 179 case DurationDay: 180 duration = time.Hour * 24 181 case DurationHour: 182 duration = time.Hour 183 case DurationMinute, DurationMinuteAbbr: 184 duration = time.Minute 185 case DurationSecond, DurationSecondAbbr: 186 duration = time.Second 187 case DurationMillisecond, DurationMillisecondAbbr: 188 duration = time.Millisecond 189 case DurationMicrosecond, DurationMicrosecondAbbr: 190 duration = time.Microsecond 191 case DurationNanosecond, DurationNanosecondAbbr: 192 duration = time.Nanosecond 193 default: 194 return 0, fmt.Errorf("unsupported unit: %v", unit) 195 } 196 return time.Duration(value) * duration, nil 197 } 198 199 const ( 200 eofToken = -1 201 invalidToken = iota 202 timeValueToken 203 nowToken 204 yesterdayToken 205 tomorrowToken 206 whitespacesToken 207 durationToken 208 inTimezoneToken 209 durationPluralToken 210 positiveModifierToken 211 negativeModifierToken 212 timezoneToken 213 ) 214 215 var timeAtExpressionMatchers = map[int]Matcher{ 216 timeValueToken: NewIntMatcher(), 217 whitespacesToken: CharactersMatcher{" \n\t"}, 218 durationToken: NewKeywordsMatcher(false, DurationWeek, DurationDay, DurationHour, DurationMinute, DurationMinuteAbbr, DurationSecond, DurationSecondAbbr, DurationMillisecond, DurationMillisecondAbbr, DurationMicrosecond, DurationMicrosecondAbbr, DurationNanosecond, DurationNanosecondAbbr), 219 durationPluralToken: NewKeywordsMatcher(false, "s"), 220 nowToken: NewKeywordsMatcher(false, Now), 221 yesterdayToken: NewKeywordsMatcher(false, Yesterday), 222 tomorrowToken: NewKeywordsMatcher(false, Tomorrow), 223 positiveModifierToken: NewKeywordsMatcher(false, "onward", "ahead", "after", "later", "in the future", "inthefuture"), 224 negativeModifierToken: NewKeywordsMatcher(false, "past", "ago", "before", "earlier", "in the past", "inthepast"), 225 inTimezoneToken: NewKeywordsMatcher(false, "in"), 226 timezoneToken: NewRemainingSequenceMatcher(), 227 eofToken: &EOFMatcher{}, 228 } 229 230 //TimeAt returns time of now supplied offsetExpression, this function uses TimeDiff 231 func TimeAt(offsetExpression string) (*time.Time, error) { 232 return TimeDiff(time.Now(), offsetExpression) 233 } 234 235 //TimeDiff returns time for supplied base time and literal, the supported literal now, yesterday, tomorrow, or the following template: 236 // - [timeValueToken] durationToken past_or_future_modifier [IN tz] 237 // where time modifier can be any of the following: "onward", "ahead", "after", "later", or "past", "ago", "before", "earlier", "in the future", "in the past") ) 238 func TimeDiff(base time.Time, expression string) (*time.Time, error) { 239 if expression == "" { 240 return nil, fmt.Errorf("expression was empty") 241 } 242 var delta time.Duration 243 var isNegative = false 244 245 tokenizer := NewTokenizer(expression, invalidToken, eofToken, timeAtExpressionMatchers) 246 var val = 1 247 var isTimeExtracted = false 248 token, err := ExpectToken(tokenizer, "", timeValueToken, nowToken, yesterdayToken, tomorrowToken) 249 if err == nil { 250 switch token.Token { 251 case timeValueToken: 252 val, _ = ToInt(token.Matched) 253 case yesterdayToken: 254 isNegative = true 255 fallthrough 256 case tomorrowToken: 257 delta, _ = NewDuration(1, DurationDay) 258 fallthrough 259 case nowToken: 260 isTimeExtracted = true 261 } 262 } 263 264 if !isTimeExtracted { 265 token, err = ExpectTokenOptionallyFollowedBy(tokenizer, whitespacesToken, "expected time unit", durationToken) 266 if err != nil { 267 return nil, err 268 } 269 delta, _ = NewDuration(val, strings.ToLower(token.Matched)) 270 _, _ = ExpectToken(tokenizer, "", durationPluralToken) 271 token, err = ExpectTokenOptionallyFollowedBy(tokenizer, whitespacesToken, "expected time modifier", positiveModifierToken, negativeModifierToken) 272 if err != nil { 273 return nil, err 274 } 275 if token.Token == negativeModifierToken { 276 isNegative = true 277 } 278 } 279 280 if token, err = ExpectTokenOptionallyFollowedBy(tokenizer, whitespacesToken, "expected in", inTimezoneToken); err == nil { 281 token, err = ExpectToken(tokenizer, "epected timezone", timezoneToken) 282 if err != nil { 283 return nil, err 284 } 285 tz := strings.TrimSpace(token.Matched) 286 tzLocation, err := time.LoadLocation(tz) 287 if err != nil { 288 return nil, fmt.Errorf("failed to load timezone tzLocation: %v, %v", tz, err) 289 } 290 base = base.In(tzLocation) 291 } 292 token, err = ExpectToken(tokenizer, "expected eofToken", eofToken) 293 if err != nil { 294 return nil, err 295 } 296 if isNegative { 297 delta *= -1 298 } 299 base = base.Add(delta) 300 return &base, nil 301 } 302 303 //ElapsedToday returns elapsed today time percent, it takes optionally timezone 304 func ElapsedToday(tz string) (float64, error) { 305 if tz != "" { 306 tz = "In" + tz 307 } 308 now, err := TimeAt("now" + tz) 309 if err != nil { 310 return 0, err 311 } 312 return ElapsedDay(*now), nil 313 } 314 315 //ElapsedDay returns elapsed pct for passed in day (second elapsed that day over 24 hours) 316 func ElapsedDay(dateTime time.Time) float64 { 317 elapsedToday := time.Duration(dateTime.Hour())*time.Hour + time.Duration(dateTime.Minute())*time.Minute + time.Duration(dateTime.Second()) + time.Second 318 elapsedTodayPct := float64(elapsedToday) / float64((24 * time.Hour)) 319 return elapsedTodayPct 320 } 321 322 //RemainingToday returns remaining today time percent, it takes optionally timezone 323 func RemainingToday(tz string) (float64, error) { 324 elapsedToday, err := ElapsedToday(tz) 325 if err != nil { 326 return 0, err 327 } 328 return 1.0 - elapsedToday, nil 329 } 330 331 //TimeWindow represents a time window 332 type TimeWindow struct { 333 Loopback *Duration 334 StartDate string 335 startTime *time.Time 336 EndDate string 337 endTime *time.Time 338 TimeLayout string 339 TimeFormat string 340 Interval *Duration 341 } 342 343 //Range iterates with interval step between start and window end. 344 func (w *TimeWindow) Range(handler func(time time.Time) (bool, error)) error { 345 start, err := w.StartTime() 346 if err != nil { 347 return err 348 } 349 350 end, err := w.EndTime() 351 if err != nil { 352 return err 353 } 354 if w.Interval == nil && w.Loopback != nil { 355 w.Interval = w.Loopback 356 } 357 358 if w.Interval == nil { 359 _, err = handler(*end) 360 return err 361 } 362 interval, err := w.Interval.Duration() 363 if err != nil { 364 return err 365 } 366 for ts := *start; ts.Before(*end) || ts.Equal(*end); ts = ts.Add(interval) { 367 if ok, err := handler(ts); err != nil || !ok { 368 return err 369 } 370 } 371 return err 372 } 373 374 //Layout return time layout 375 func (w *TimeWindow) Layout() string { 376 if w.TimeLayout != "" { 377 return w.TimeLayout 378 } 379 if w.TimeFormat != "" { 380 w.TimeLayout = DateFormatToLayout(w.TimeFormat) 381 } 382 if w.TimeLayout == "" { 383 w.TimeLayout = time.RFC3339 384 } 385 return w.TimeLayout 386 } 387 388 //StartTime returns time window start time 389 func (w *TimeWindow) StartTime() (*time.Time, error) { 390 if w.StartDate != "" { 391 if w.startTime != nil { 392 return w.startTime, nil 393 } 394 timeLayout := w.Layout() 395 startTime, err := time.Parse(timeLayout, w.StartDate) 396 if err != nil { 397 return nil, err 398 } 399 w.startTime = &startTime 400 return w.startTime, nil 401 } 402 endDate, err := w.EndTime() 403 if err != nil { 404 return nil, err 405 } 406 if w.Loopback == nil || w.Loopback.Value == 0 { 407 return endDate, nil 408 } 409 loopback, err := w.Loopback.Duration() 410 if err != nil { 411 return nil, err 412 } 413 startTime := endDate.Add(-loopback) 414 return &startTime, nil 415 } 416 417 //EndTime returns time window end time 418 func (w *TimeWindow) EndTime() (*time.Time, error) { 419 if w.EndDate != "" { 420 if w.endTime != nil { 421 return w.endTime, nil 422 } 423 timeLayout := w.Layout() 424 endTime, err := time.Parse(timeLayout, w.EndDate) 425 if err != nil { 426 return nil, err 427 } 428 w.endTime = &endTime 429 return w.endTime, nil 430 } 431 now := time.Now() 432 return &now, nil 433 }