date.scm 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. ;;; guile-webutils -- Web application utilities for Guile
  2. ;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
  3. ;;;
  4. ;;; This program is free software: you can redistribute it and/or
  5. ;;; modify it under the terms of the GNU General Public License
  6. ;;; as published by the Free Software Foundation, either version 3 of
  7. ;;; the License, or (at your option) any later version.
  8. ;;;
  9. ;;; This program is distributed in the hope that it will be useful,
  10. ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. ;;; General Public License for more details.
  13. ;;;
  14. ;;; You should have received a copy of the GNU General Public License
  15. ;;; along with this program. If not, see
  16. ;;; <http://www.gnu.org/licenses/>.
  17. (define-module (webutils date)
  18. #:use-module (ice-9 format)
  19. #:use-module (ice-9 match)
  20. #:use-module (srfi srfi-19)
  21. #:use-module (rx irregex)
  22. #:export (date->rfc3339-string rfc3339-string->date
  23. date->http-date-string http-date-string->date))
  24. ;;; TODO: It's pretty absurd that we're using irregex for this.
  25. ;;; We could make this more performant and drop a dependency by writing
  26. ;;; a manual parser or using string-based regular expressions.
  27. ;;;
  28. ;;; A not particularly fast but nice looking implementation of RFC 3339
  29. ;;; using irregex.
  30. ;;; My 10 year old laptop can parse about 6k dates / second.
  31. ;;; Maybe a lot faster in guile 2.2? :)
  32. ;;;
  33. (define* (digit=> to-var how-many)
  34. `(=> ,to-var (= ,how-many numeric)))
  35. ;; date-fullyear = 4DIGIT
  36. ;; date-month = 2DIGIT ; 01-12
  37. ;; date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
  38. ;; ; month/year
  39. ;; time-hour = 2DIGIT ; 00-23
  40. ;; time-minute = 2DIGIT ; 00-59
  41. ;; time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
  42. ;; ; rules
  43. ;; time-secfrac = "." 1*DIGIT
  44. ;; time-numoffset = ("+" / "-") time-hour ":" time-minute
  45. ;; time-offset = "Z" / time-numoffset
  46. ;;
  47. ;; partial-time = time-hour ":" time-minute ":" time-second
  48. ;; [time-secfrac]
  49. ;; full-date = date-fullyear "-" date-month "-" date-mday
  50. ;; full-time = partial-time time-offset
  51. ;;
  52. ;; date-time = full-date "T" full-time
  53. (define hour-sre
  54. (digit=> 'hour 2))
  55. (define minute-sre
  56. (digit=> 'minute 2))
  57. (define second-sre
  58. (digit=> 'second 2))
  59. (define secfrac-sre
  60. '(: "." (=> secfrac (+ numeric))))
  61. (define time-numoffset
  62. `(: (=> offset-plusminus (or "+" "-"))
  63. ,(digit=> 'offset-hour 2) ":"
  64. ,(digit=> 'offset-minute 2)))
  65. (define time-offset
  66. `(or "Z"
  67. ,time-numoffset))
  68. (define date-fullyear-sre
  69. (digit=> 'fullyear 4))
  70. (define date-month-sre
  71. (digit=> 'month 2))
  72. (define date-mday-sre
  73. (digit=> 'mday 2))
  74. (define full-date-sre
  75. `(: ,date-fullyear-sre "-" ,date-month-sre "-" ,date-mday-sre))
  76. ;;; AS2 specific version of RFC3339
  77. ;;; This permits that seconds MAY be omitted
  78. ;; as2-partial-time = time-hour ":" time-minute [":" time-second]
  79. ;; [time-secfrac]
  80. ;; as2-full-time = as2-partial-time time-offset
  81. ;; as2-date-time = full-date "T" as2-full-time
  82. (define partial-time-sre
  83. `(: ,hour-sre ":" ,minute-sre
  84. (? ":" ,second-sre)
  85. (? ,secfrac-sre)))
  86. (define full-time-sre
  87. `(: ,partial-time-sre ,time-offset))
  88. (define date-time-sre
  89. `(: ,full-date-sre "T" ,full-time-sre))
  90. (define date-time-irx
  91. (sre->irregex date-time-sre))
  92. (define (rfc3339-string->date str)
  93. "Convert an RFC3339 formatted date string into an srfi-19 date type."
  94. (define (rx-match->date rx-match)
  95. (define (rx-part name)
  96. (irregex-match-substring rx-match name))
  97. (define (rx-number name)
  98. (and=> (rx-part name)
  99. string->number))
  100. (let ((nsecs (min (or (rx-number 'secfrac) 0)
  101. 999999999))
  102. (seconds (or (rx-number 'second) 0))
  103. (minutes (rx-number 'minute))
  104. (hours (rx-number 'hour))
  105. (date (rx-number 'mday))
  106. (month (rx-number 'month))
  107. (year (rx-number 'fullyear))
  108. (offset (let ((plusminus (rx-part 'offset-plusminus))
  109. (offset-hour (rx-number 'offset-hour))
  110. (offset-minute (rx-number 'offset-minute)))
  111. (match plusminus
  112. ;; No offset, return 0 seconds
  113. (#f 0)
  114. ;; Positive offset
  115. ("+"
  116. (+ (* offset-hour 60 60)
  117. (* offset-minute 60)))
  118. ;; negative offset
  119. ("-"
  120. (* (+ (* offset-hour 60 60)
  121. (* offset-minute 60))
  122. -1))))))
  123. (make-date nsecs seconds minutes hours date month year offset)))
  124. (and=> (irregex-match date-time-irx str)
  125. rx-match->date))
  126. ;; @@: Well, this isn't very fast either. Only about 5k / second.
  127. ;; I guess Guile 2.0 isn't very fast with strings :)
  128. (define (date->rfc3339-string date)
  129. "Convert an srfi-19 date type into an RFC3339 formatted date string."
  130. (define (format-2-digits digit)
  131. (format #f "~2,'0d" digit))
  132. (string-append
  133. (format #f "~4,'0d-~2,'0d-~2,'0dT~2,'0d:~2,'0d:~2,'0d"
  134. (date-year date)
  135. (date-month date)
  136. (date-day date)
  137. (date-hour date)
  138. (date-minute date)
  139. (date-second date))
  140. ;; Append nanoseconds, if appropriate
  141. (let ((nsec (date-nanosecond date)))
  142. (cond ((and nsec ; @@: is date-nanosecond ever #f?
  143. (not (eqv? nsec 0)))
  144. (string-append "." (number->string nsec)))
  145. (else "")))
  146. (let ((offset (date-zone-offset date)))
  147. (cond ((and offset (not (eqv? offset 0)))
  148. (let* ((abs-offset (abs offset))
  149. (hour (floor (/ abs-offset (* 60 60))))
  150. (minute (floor (/ (- abs-offset (* hour 60 60)) 60))))
  151. (string-append (if (< offset 0)
  152. "-" "+")
  153. (format-2-digits hour) ":"
  154. (format-2-digits minute))))
  155. (else "Z")))))
  156. ;;; HTTP style dates
  157. ;;; ================
  158. (define http-parse-date
  159. (@@ (web http) parse-date))
  160. (define http-write-date
  161. (@@ (web http) write-date))
  162. (define (http-date-string->date str)
  163. "Parse any of the date types defined in RFC2616 sec 3.3.1 into a <date>"
  164. (catch 'bad-header
  165. (lambda ()
  166. (catch 'bad-header-component
  167. (lambda ()
  168. (http-parse-date str))
  169. (const #f)))
  170. (const #f)))
  171. (define (date->http-date-string date)
  172. "Parse a <date> into a string acceptable for HTTP headers, as defined
  173. by RFC2616."
  174. (with-output-to-string
  175. (lambda ()
  176. (http-write-date date (current-output-port)))))