...

Source file src/github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel/parse_time.go

Documentation: github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel

     1  package ldmodel
     2  
     3  import (
     4  	"time"
     5  	"unicode"
     6  )
     7  
     8  // A fast, zero-heap-allocations implementation of RFC3339 date/time parsing
     9  // (https://tools.ietf.org/html/rfc3339). The returned Time always has UTC as its Location -
    10  // it still respects any time zone specifier but simply adds that offset to the UTC time.
    11  //
    12  // The following deviations from the RFC3339 spec are intentional, in order to be consistent
    13  // with the behavior of Go's time.Parse() with the time.RFC3339 format. They are marked in
    14  // the code with "// NONSTANDARD".
    15  //
    16  // - There cannot be more than 9 digits of fractional seconds (the spec does not define any
    17  // maximum length).
    18  //
    19  // - In a time zone offset of -hh:mm or +hh:mm, hh must be an integer but has no maximum
    20  // (the spec defines a maximum of 23).
    21  //
    22  // - The hour can be either 1 digit or 2 digits (the spec requires 2).
    23  func parseRFC3339TimeUTC(s string) (time.Time, bool) {
    24  	scanner := newSimpleASCIIScanner(s)
    25  
    26  	year, _, ok := parseDateTimeNumericField(&scanner, hyphenTerminator, false, 4, 4, 0, 9999)
    27  	if !ok {
    28  		return time.Time{}, false
    29  	}
    30  	month, _, ok := parseDateTimeNumericField(&scanner, hyphenTerminator, false, 2, 2, 1, 12)
    31  	if !ok {
    32  		return time.Time{}, false
    33  	}
    34  	day, _, ok := parseDateTimeNumericField(&scanner, tTerminator, false, 2, 2, 1, 31)
    35  	if !ok {
    36  		return time.Time{}, false
    37  	}
    38  	hour, _, ok := parseDateTimeNumericField(&scanner, colonTerminator, false, 1, 2, 0, 23)
    39  	// NONSTANDARD: time.Parse allows 1-digit hour
    40  	if !ok {
    41  		return time.Time{}, false
    42  	}
    43  	minute, _, ok := parseDateTimeNumericField(&scanner, colonTerminator, false, 2, 2, 0, 59)
    44  	if !ok {
    45  		return time.Time{}, false
    46  	}
    47  	second, term, ok := parseDateTimeNumericField(&scanner, endOfSecondsTerminator, false, 2, 2, 0, 60)
    48  	// note that second can be 60 sometimes due to leap seconds
    49  	if !ok {
    50  		return time.Time{}, false
    51  	}
    52  
    53  	var nanos int
    54  	if term == '.' {
    55  		var fractionStr string
    56  		fractionStr, term = scanner.readUntil(endOfFractionalSecondsTerminator)
    57  		if term < 0 || len(fractionStr) > 9 {
    58  			// NONSTANDARD: time.Parse does not support more than 9 fractional digits
    59  			return time.Time{}, false
    60  		}
    61  		n, ok := parsePositiveNumericString(fractionStr)
    62  		if !ok {
    63  			return time.Time{}, false
    64  		}
    65  		nanos = n
    66  		for i := len(fractionStr); i < 9; i++ {
    67  			nanos *= 10
    68  		}
    69  	}
    70  
    71  	var tzOffsetSeconds int
    72  	if term == '+' || term == '-' {
    73  		offsetHours, _, ok := parseDateTimeNumericField(&scanner, colonTerminator, false, 2, 2, 0, 99)
    74  		// NONSTANDARD: time.Parse imposes no maximum on the hour field, just a 2-digit length
    75  		if !ok {
    76  			return time.Time{}, false
    77  		}
    78  		offsetMinutes, _, ok := parseDateTimeNumericField(&scanner, noTerminator, true, 2, 2, 0, 59)
    79  		if !ok {
    80  			return time.Time{}, false
    81  		}
    82  		tzOffsetSeconds = (offsetMinutes + (offsetHours * 60)) * 60
    83  		if term == '+' {
    84  			tzOffsetSeconds = -tzOffsetSeconds
    85  		}
    86  	}
    87  
    88  	t := time.Date(year, time.Month(month), day, hour, minute, second, nanos, time.UTC)
    89  	if tzOffsetSeconds != 0 {
    90  		t = t.Add(time.Second * time.Duration(tzOffsetSeconds))
    91  	}
    92  
    93  	return t, true
    94  }
    95  
    96  func hyphenTerminator(ch rune) bool { return ch == '-' }
    97  func tTerminator(ch rune) bool      { return ch == 't' || ch == 'T' }
    98  func colonTerminator(ch rune) bool  { return ch == ':' }
    99  func endOfSecondsTerminator(ch rune) bool {
   100  	return ch == '.' || ch == 'Z' || ch == 'z' || ch == '+' || ch == '-'
   101  }
   102  func endOfFractionalSecondsTerminator(ch rune) bool {
   103  	return ch == 'Z' || ch == 'z' || ch == '+' || ch == '-'
   104  }
   105  
   106  func parseDateTimeNumericField(
   107  	scanner *simpleASCIIScanner,
   108  	terminatorFn func(rune) bool,
   109  	eofOK bool,
   110  	minLength, maxLength, minValue, maxValue int,
   111  ) (int, int8, bool) {
   112  	s, term := scanner.readUntil(terminatorFn)
   113  	if s == "" || (!eofOK && term < 0) {
   114  		return 0, term, false
   115  	}
   116  	length := len(s)
   117  	if length < minLength || length > maxLength {
   118  		return 0, term, false
   119  	}
   120  	n, ok := parsePositiveNumericString(s)
   121  	if !ok || n < minValue || n > maxValue {
   122  		return 0, term, false
   123  	}
   124  	return n, term, true
   125  }
   126  
   127  // Attempts to parse a string as an integer greater than or equal to zero. Non-ASCII strings are not supported.
   128  func parsePositiveNumericString(s string) (int, bool) {
   129  	max := len(s)
   130  	if max == 0 {
   131  		return 0, false
   132  	}
   133  	n := 0
   134  	for i := 0; i < max; i++ {
   135  		ch := rune(s[i])
   136  		if ch < '0' || ch > '9' {
   137  			return 0, false
   138  		}
   139  		n = n*10 + int(ch-'0')
   140  	}
   141  	return n, true
   142  }
   143  
   144  // An extremely simple tokenizing helper that only handles ASCII strings.
   145  
   146  type simpleASCIIScanner struct {
   147  	source string
   148  	length int
   149  	pos    int
   150  }
   151  
   152  const (
   153  	scannerEOF      int8 = -1
   154  	scannerNonASCII int8 = -2
   155  )
   156  
   157  func noTerminator(rune) bool {
   158  	return false
   159  }
   160  
   161  func newSimpleASCIIScanner(source string) simpleASCIIScanner {
   162  	return simpleASCIIScanner{source: source, length: len(source)}
   163  }
   164  
   165  func (s *simpleASCIIScanner) peek() int8 {
   166  	if s.pos >= s.length {
   167  		return scannerEOF
   168  	}
   169  	var ch uint8 = s.source[s.pos] //nolint:stylecheck
   170  	if ch == 0 || ch > unicode.MaxASCII {
   171  		return scannerNonASCII
   172  	}
   173  	return int8(ch)
   174  }
   175  
   176  func (s *simpleASCIIScanner) next() int8 {
   177  	ch := s.peek()
   178  	if ch > 0 {
   179  		s.pos++
   180  	}
   181  	return ch
   182  }
   183  
   184  func (s *simpleASCIIScanner) readUntil(terminatorFn func(rune) bool) (substring string, terminatedBy int8) {
   185  	startPos := s.pos
   186  	var ch int8
   187  	for {
   188  		ch = s.next()
   189  		if ch < 0 || terminatorFn(rune(ch)) {
   190  			break
   191  		}
   192  	}
   193  	endPos := s.pos
   194  	if ch > 0 {
   195  		endPos--
   196  	}
   197  	return s.source[startPos:endPos], ch
   198  }
   199  

View as plain text