iso8601.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package utils
  3. import (
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "time"
  8. )
  9. var _ = fmt.Print
  10. func is_digit(x byte) bool {
  11. return '0' <= x && x <= '9'
  12. }
  13. // The following is copied from the Go standard library to implement date range validation logic
  14. // equivalent to the behaviour of Go's time.Parse.
  15. func isLeap(year int) bool {
  16. return year%4 == 0 && (year%100 != 0 || year%400 == 0)
  17. }
  18. // daysInMonth is the number of days for non-leap years in each calendar month starting at 1
  19. var daysInMonth = [13]int{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
  20. func daysIn(m time.Month, year int) int {
  21. if m == time.February && isLeap(year) {
  22. return 29
  23. }
  24. return daysInMonth[int(m)]
  25. }
  26. func ISO8601Parse(raw string) (time.Time, error) {
  27. orig := raw
  28. raw = strings.TrimSpace(raw)
  29. required_number := func(num_digits int) (int, error) {
  30. if len(raw) < num_digits {
  31. return 0, fmt.Errorf("Insufficient digits")
  32. }
  33. text := raw[:num_digits]
  34. raw = raw[num_digits:]
  35. ans, err := strconv.ParseUint(text, 10, 32)
  36. return int(ans), err
  37. }
  38. optional_separator := func(x byte) bool {
  39. if len(raw) > 0 && raw[0] == x {
  40. raw = raw[1:]
  41. }
  42. return len(raw) > 0 && is_digit(raw[0])
  43. }
  44. errf := func(msg string) (time.Time, error) {
  45. return time.Time{}, fmt.Errorf("Invalid ISO8601 timestamp: %#v. %s", orig, msg)
  46. }
  47. optional_separator('+')
  48. year, err := required_number(4)
  49. if err != nil {
  50. return errf("timestamp does not start with a 4 digit year")
  51. }
  52. var month int = 1
  53. var day int = 1
  54. if optional_separator('-') {
  55. month, err = required_number(2)
  56. if err != nil {
  57. return errf("timestamp does not have a valid 2 digit month")
  58. }
  59. if optional_separator('-') {
  60. day, err = required_number(2)
  61. if err != nil {
  62. return errf("timestamp does not have a valid 2 digit day")
  63. }
  64. }
  65. }
  66. var hour, minute, second, nsec int
  67. if len(raw) > 0 && (raw[0] == 'T' || raw[0] == ' ') {
  68. raw = raw[1:]
  69. hour, err = required_number(2)
  70. if err != nil {
  71. return errf("timestamp does not have a valid 2 digit hour")
  72. }
  73. if optional_separator(':') {
  74. minute, err = required_number(2)
  75. if err != nil {
  76. return errf("timestamp does not have a valid 2 digit minute")
  77. }
  78. if optional_separator(':') {
  79. second, err = required_number(2)
  80. if err != nil {
  81. return errf("timestamp does not have a valid 2 digit second")
  82. }
  83. }
  84. }
  85. if len(raw) > 0 && (raw[0] == '.' || raw[0] == ',') {
  86. raw = raw[1:]
  87. num_digits := 0
  88. for len(raw) > num_digits && is_digit(raw[num_digits]) {
  89. num_digits++
  90. }
  91. text := raw[:num_digits]
  92. raw = raw[num_digits:]
  93. extra := 9 - len(text)
  94. if extra < 0 {
  95. text = text[:9]
  96. }
  97. if text != "" {
  98. n, err := strconv.ParseUint(text, 10, 64)
  99. if err != nil {
  100. return errf("timestamp does not have a valid nanosecond field")
  101. }
  102. nsec = int(n)
  103. for ; extra > 0; extra-- {
  104. nsec *= 10
  105. }
  106. }
  107. }
  108. }
  109. switch {
  110. case month < 1 || month > 12:
  111. return errf("timestamp has invalid month value")
  112. case day < 1 || day > 31 || day > daysIn(time.Month(month), year):
  113. return errf("timestamp has invalid day value")
  114. case hour < 0 || hour > 23:
  115. return errf("timestamp has invalid hour value")
  116. case minute < 0 || minute > 59:
  117. return errf("timestamp has invalid minute value")
  118. case second < 0 || second > 59:
  119. return errf("timestamp has invalid second value")
  120. }
  121. loc := time.UTC
  122. tzsign, tzhour, tzminute := 0, 0, 0
  123. if len(raw) > 0 {
  124. switch raw[0] {
  125. case '+':
  126. tzsign = 1
  127. case '-':
  128. tzsign = -1
  129. }
  130. }
  131. if tzsign != 0 {
  132. raw = raw[1:]
  133. tzhour, err = required_number(2)
  134. if err != nil {
  135. return errf("timestamp has invalid timezone hour")
  136. }
  137. optional_separator(':')
  138. tzminute, err = required_number(2)
  139. if err != nil {
  140. tzminute = 0
  141. }
  142. seconds := tzhour*3600 + tzminute*60
  143. loc = time.FixedZone("", tzsign*seconds)
  144. }
  145. return time.Date(year, time.Month(month), day, hour, minute, second, nsec, loc), err
  146. }
  147. func ISO8601Format(x time.Time) string {
  148. return x.Format(time.RFC3339Nano)
  149. }