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  }