summaryrefslogtreecommitdiff
path: root/src/ptime.ml
blob: 00ff119c5bd4baca14f4dfc4a3ea30b113ff46fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
(*---------------------------------------------------------------------------
   Copyright (c) 2015 The ptime programmers. All rights reserved.
   Distributed under the ISC license, see terms at the end of the file.
  ---------------------------------------------------------------------------*)

(* Julian day and proleptic Gregorian calendar date conversion.

   Formulae are from the calendar FAQ:
   http://www.tondering.dk/claus/cal/julperiod.php#formula

   These formulae work for positive Julian days. They represent
   Gegorian calendar BCE year `y` by `-(y-1)`, e.g. 2 BCE is -1, this
   follows the convention of ISO 8601.

   All timestamps in Ptime's [min;max] range are represented by
   positive Julian days and the formulae do not overflow on 32-bit
   platforms in this restricted range. *)

let jd_to_date jd =
  let a = jd + 32044 in
  let b = (4 * a + 3) / 146097 in
  let c = a - ((146097 * b) / 4) in
  let d = (4 * c + 3) / 1461 in
  let e = c - ((1461 * d) / 4) in
  let m = (5 * e + 2) / 153 in
  let day = e - ((153 * m + 2) / 5) + 1 in
  let month = m + 3 - (12 * (m / 10)) in
  let year = 100 * b + d - 4800 + (m / 10) in
  (year, month, day)

let jd_to_year jd = (* Same as above but only for the year *)
  let a = jd + 32044 in
  let b = (4 * a + 3) / 146097 in
  let c = a - ((146097 * b) / 4) in
  let d = (4 * c + 3) / 1461 in
  let e = c - ((1461 * d) / 4) in
  let m = (5 * e + 2) / 153 in
  100 * b + d - 4800 + (m / 10)

let jd_of_date (year, month, day) =
  let a = (14 - month) / 12 in
  let y = year + 4800 - a in
  let m = month + 12 * a - 3 in
  day + ((153 * m) + 2)/ 5 + 365 * y +
  (y / 4) - (y / 100) + (y / 400) - 32045

let jd_posix_epoch = 2_440_588          (* the Julian day of the POSIX epoch *)
let jd_ptime_min = 1_721_060                  (* the Julian day of Ptime.min *)
let jd_ptime_max = 5_373_484                  (* the Julian day of Ptime.max *)

(* Picosecond precision POSIX timestamps and time span representation.

   POSIX timestamps and spans are represented by int * int64 pairs
   with the int64 in the range [0L;86_399_999_999_999_999L]. A pair
   [(d, ps)] denotes the POSIX picosecond duration [d] * 86_400e12 +
   [ps].

   For a timestamp this can be seen as a POSIX day count from the
   epoch paired with a picosecond precision POSIX time point in that
   day starting from 00:00:00.

   By definition with a negative [d] the [ps] duration brings us
   towards zero, *not* towards infinity:


         (d * 86_400e12) (d * 86_400e12 + ps)       0
     ... -----+-----------------+-------------------+--------- ...
              [---------------->|
                   ps

   [d] is largely sufficent to represent all the days in Ptime's
   [min;max] range on both 32-bit and 64-bit platforms. *)

type t = int * int64

let ps_count_in_ps    =                      1L
let ps_count_in_ns    =                  1_000L
let ps_count_in_100ns =                100_000L
let ps_count_in_us    =              1_000_000L
let ps_count_in_100us =            100_000_000L
let ps_count_in_ms    =          1_000_000_000L
let ps_count_in_100ms =        100_000_000_000L
let ps_count_in_s     =      1_000_000_000_000L
let ps_count_in_min   =     60_000_000_000_000L
let ps_count_in_hour  =   3600_000_000_000_000L
let ps_count_in_day   = 86_400_000_000_000_000L
let ps_day_max        = 86_399_999_999_999_999L

let day_min = jd_ptime_min - jd_posix_epoch
let day_max = jd_ptime_max - jd_posix_epoch

let epoch = (0, 0L)             (* 1970-01-01 00:00:00 UTC *)
let min = (day_min, 0L)         (* 0000-01-01 00:00:00 UTC *)
let max = (day_max, ps_day_max) (* 9999-12-31 23:59:59 UTC *)

(* POSIX time spans *)

type span = t

module Span = struct

  let stdlib_abs = abs

  (* Arithmetic *)

  let neg = function
  | (d, 0L)  -> (-d, 0L)
  | (d, ps) -> (-(d + 1), Int64.sub ps_count_in_day ps)

  let add (d0, ps0) (d1, ps1) =
    let d = d0 + d1 in
    let ps = Int64.add ps0 ps1 in
    let ps_clamp = Int64.rem ps ps_count_in_day in
    let d = d + Int64.compare ps ps_clamp in
    d, ps_clamp

  let sub s0 s1 = add s0 (neg s1)
  let abs (d, _ as s) = if d < 0 then neg s else s

  (* POSIX time spans *)

  type t = span

  let zero = (0, 0L)
  let v (d, ps as s) =
    if ps < 0L || ps > ps_day_max
    then invalid_arg (Format.sprintf "illegal ptime time span: (%d,%Ld)" d ps)
    else s

  let of_d_ps (d, ps as s) = if ps < 0L || ps > ps_day_max then None else Some s
  let unsafe_of_d_ps s = s
  let unsafe_of_d_ps_option s = s
  let to_d_ps s = s

  let of_int_s secs =
    let d = stdlib_abs secs in
    let s = (d / 86_400, Int64.(mul (of_int (d mod 86_400)) ps_count_in_s)) in
    if secs < 0 then neg s else s

  let day_int_min = min_int / 86_400
  let day_int_max = max_int / 86_400
  let to_int_s (d, ps) =
    if d < day_int_min || d > day_int_max then None else
    let days_s = d * 86_400 in
    let day_s = Int64.(to_int (div ps ps_count_in_s)) (* always positive *) in
    let secs = days_s + day_s in
    if secs < days_s (* positive overflow *) then None else Some secs

  let min_int_float = float min_int
  let max_int_float = float max_int
  let of_float_s secs =
    if secs <> secs (* nan *) then None else
    let days = floor (secs /. 86_400.) in
    if days < min_int_float || days > max_int_float then None else
    let rem_s = mod_float secs 86_400. in
    let rem_s = if rem_s < 0. then 86_400. +. rem_s else rem_s in
    if rem_s >= 86_400. then
      (* Guard against a potential overflow in the computation of [rem_s] *)
      let days = days +. 1. in
      if days > max_int_float then None else
      Some (int_of_float days, 0L)
    else
    let frac_s, rem_s = modf rem_s in
    let rem_ps = Int64.(mul (of_float rem_s) ps_count_in_s) in
    let frac_ps = Int64.(of_float (frac_s *. 1e12)) in
    Some (int_of_float days, (Int64.add rem_ps frac_ps))

  let to_float_s (d, ps) =
    let days_s = (float d) *. 86_400. in
    let day_s = Int64.(to_float (div ps ps_count_in_s)) in
    let day_rem_ps = Int64.(to_float (rem ps ps_count_in_s)) in
    days_s +. day_s +. (day_rem_ps *. 1e-12)

  (* Predicates *)

  let equal (d0, ps0) (d1, ps1) =
    (compare : int -> int -> int) d0 d1 = 0 &&
    Int64.compare ps0 ps1 = 0

  let compare (d0, ps0) (d1, ps1) =
    let c = (compare : int -> int -> int) d0 d1 in
    if c <> 0 then c else (compare : int64 -> int64 -> int) ps0 ps1

  (* Rounding *)

  let round_div a b = (* a >= 0 and b > 0 *)
    if a = 0L then 0L else
    Int64.(div (add a (div b 2L)) b)

  let frac_div = [| 1_000_000_000_000L;
                      100_000_000_000L;
                       10_000_000_000L;
                        1_000_000_000L;
                          100_000_000L;
                           10_000_000L;
                            1_000_000L;
                              100_000L;
                               10_000L;
                                1_000L;
                                  100L;
                                   10L;
                                    1L; |]

  let round ~frac_s:frac (sign, _ as t) =
    let frac = if frac < 0 then 0 else (if frac > 12 then 12 else frac) in
    let (d, ps) = if sign < 0 then neg t else t in
    let rps = Int64.mul (round_div ps frac_div.(frac)) frac_div.(frac) in
    let t = if rps > ps_day_max then (d + 1, 0L) else (d, rps) in
    if sign < 0 then neg t else t

  let truncate ~frac_s:frac (sign, _ as t) =
    let frac = if frac < 0 then 0 else (if frac > 12 then 12 else frac) in
    let (d, ps) = if sign < 0 then neg t else t in
    let tps = Int64.(sub ps (rem ps frac_div.(frac))) in
    if sign < 0 then neg (d, tps) else (d, tps)

  let truncate_down ~frac_s:frac (d, ps) =
    (d, Int64.(sub ps (rem ps frac_div.(frac ))))

  (* Pretty printing *)

  let dump ppf (d, ps) = Format.fprintf ppf "@[<1>(%d,@,%Ld)@]" d ps

  (* Warning laborious code follows. Is there a better way ? *)

  let divide_ps ~carry ps hi lo =
    let hi_d = Int64.(to_int (div ps hi)) in
    let rem_ps = Int64.rem ps hi in
    let lo_d = Int64.to_int (round_div rem_ps lo) in
    if lo_d = carry then hi_d + 1, 0 else hi_d, lo_d

  let pp_y_d ppf ~neg d ps = (* assert d >= 0 *)
    let y, rem_d =
      let max_d = max_int / 4 in
      if d > max_d then (* d * 4 overflows *) d / 365, d mod 365 else
      let y = (d * 4) / 1461 (* / 365.25 *) in
      y, d - (y * 1461) / 4
    in
    let days = rem_d + Int64.to_int (round_div ps ps_count_in_day) in
    let y, days = if days = 366 then y + 1, 1 else y, days in
    let y = if neg then -y else y in
    Format.fprintf ppf "%dy" y;
    if days <> 0 then Format.fprintf ppf "%dd" days;
    ()

  let pp_d_h ppf ~neg d ps =
    let h, _ = divide_ps ~carry:1 ps ps_count_in_hour ps_count_in_hour in
    let d, h = if h = 24 then d + 1, 0 else d, h in
    if d = 366 then Format.fprintf ppf "%dy1d" (if neg then -1 else 1) else
    if d = 365 && h >= 6
    then Format.fprintf ppf "%dy" (if neg then -1 else 1) else
    let d = if neg then -d else d in
    Format.fprintf ppf "%dd" d;
    if h <> 0 then Format.fprintf ppf "%dh" h;
    ()

  let pp_h_m ppf ~neg ps =
    let h, m = divide_ps ~carry:60 ps ps_count_in_hour ps_count_in_min in
    if h = 24 then Format.fprintf ppf "%dd" (if neg then -1 else 1) else
    let h = if neg then -h else h in
    Format.fprintf ppf "%dh" h;
    if m <> 0 then Format.fprintf ppf "%dmin" m;
    ()

  let pp_m_s ppf ~neg ps =
    let m, s = divide_ps ~carry:60 ps ps_count_in_min ps_count_in_s in
    if m = 60 then Format.fprintf ppf "%dh" (if neg then -1 else 1) else
    let m = if neg then -m else m in
    Format.fprintf ppf "%dmin" m;
    if s <> 0 then Format.fprintf ppf "%ds" s;
    ()

  let pp_s ppf ~neg ps =
    let s, ms = divide_ps ~carry:1000 ps ps_count_in_s ps_count_in_ms in
    if s = 60 then Format.fprintf ppf "%dmin" (if neg then -1 else 1) else
    let s = if neg then -s else s in
    if ms <> 0 then Format.fprintf ppf "%d.%ds" s ms else
    Format.fprintf ppf "%ds" s

  let pp_unit higher_str hi hi_str frac_limit lo ppf ~neg ps =
    let pp_unit_integral ppf ~neg h =
      if h = 1000
      then Format.fprintf ppf "%d%s" (if neg then -1 else 1) higher_str
      else Format.fprintf ppf "%d%s" (if neg then -h else h) hi_str
    in
    if ps < frac_limit then begin
      let h, l = divide_ps ~carry:1000 ps hi lo in
      if h >= 100 || l = 0 then pp_unit_integral ppf ~neg h else
      let h = if neg then -h else h in
      Format.fprintf ppf "%d.%d%s" h l hi_str
    end else begin
      let ms, _ = divide_ps ~carry:1 ps hi hi in
      pp_unit_integral ppf ~neg ms
    end

  let pp_ms =
    pp_unit "s" ps_count_in_ms "ms" ps_count_in_100ms ps_count_in_us

  let pp_us =
    pp_unit "ms" ps_count_in_us "us" ps_count_in_100us ps_count_in_ns

  let pp_ns =
    pp_unit "us" ps_count_in_ns "ns" ps_count_in_100ns ps_count_in_ps

  let pp_ps ppf ~neg ps =
    let ps = Int64.to_int ps in
    Format.fprintf ppf "%dps" (if neg then -ps else ps)

  let pp ppf (sign, _ as s) =
    let neg = sign < 0 in
    match (abs s) with
    | (0, ps) ->
        if ps >= ps_count_in_hour then pp_h_m ppf ~neg ps else
        if ps >= ps_count_in_min then pp_m_s ppf ~neg ps else
        if ps >= ps_count_in_s then pp_s ppf ~neg ps else
        if ps >= ps_count_in_ms then pp_ms ppf ~neg ps else
        if ps >= ps_count_in_us then pp_us ppf ~neg ps else
        if ps >= ps_count_in_ns then pp_ns ppf ~neg ps else
        pp_ps ppf ~neg ps
    | (d, ps) ->
        if d > 365 then pp_y_d ppf ~neg d ps else
        pp_d_h ppf ~neg d ps
end

(* POSIX timestamps *)

let v (d, ps as s) =
  if (ps < 0L || ps > ps_day_max || d < day_min || d > day_max)
  then invalid_arg (Format.sprintf "illegal ptime timestamp: (%d,%Ld)" d ps)
  else s

let unsafe_of_d_ps s = s

let of_span (d, _ as span) =
  if d < day_min || d > day_max then None else Some span

let to_span t = t

let of_float_s secs = match Span.of_float_s secs with
| None -> None
| Some d -> of_span d

let to_float_s = Span.to_float_s

let truncate = Span.truncate_down

let frac_s (_, ps) = (0, Int64.(rem ps ps_count_in_s))

(* Predicates *)

let equal = Span.equal
let compare = Span.compare
let is_earlier t ~than = compare t than = -1
let is_later t ~than = compare t than = 1

(* POSIX arithmetic *)

let add_span t d = of_span (Span.add t d)
let sub_span t d = of_span (Span.sub t d)
let diff t1 t0 = Span.sub t1 t0

(* Time zone offsets between local and UTC timelines *)

type tz_offset_s = int

(* Date-time conversion

   POSIX time counts seconds since 1970-01-01 00:00:00 UTC without
   counting leap seconds -- when a leap second occurs a POSIX second
   can be two SI seconds or zero SI second. Hence 86400 POSIX seconds
   always represent an UTC day and the translations below are accurate
   without having to refer to a leap seconds table. *)

type date = (int * int * int)
type time = (int * int * int) * tz_offset_s

let max_month_day =               (* max day number in a given year's month. *)
  let is_leap_year y = (y mod 4 = 0) && (y mod 100 <> 0 || y mod 400 = 0) in
  let mlen = [|31; 28 (* or not *); 31; 30; 31; 30; 31; 31; 30; 31; 30; 31|] in
  fun y m -> if (m = 2 && is_leap_year y) then 29 else mlen.(m - 1)

let is_date_valid (y, m, d) =
  0 <= y && y <= 9999 &&
  1 <= m && m <= 12 &&
  1 <= d && d <= max_month_day y m

let is_time_valid ((hh, mm, ss), _) =
  0 <= hh && hh <= 23 &&
  0 <= mm && mm <= 59 &&
  0 <= ss && ss <= 60

let of_date_time (date, ((hh, mm, ss), tz_offset_s as t)) =
  (* We first verify that the given date and time are Ptime-valid.
     Once this has been established we find find the number of Julian
     days since the epoch for the given proleptic Georgian calendar
     date. This gives us the POSIX day component of the timestamp. The
     remaining time fields are used to derive the picosecond precision
     time in that day compensated by the time zone offset. The final
     result is checked to be in Ptime's [min;max] range.

     By definition POSIX timestamps cannot represent leap seconds.
     With the code below any date-time with a seconds value of 60
     (leap second addition) is mapped to the POSIX timestamp that
     happens 1 second later which is what POSIX mktime would to. Any
     formally non-existing UTC date-time with a seconds value of 59
     (leap second subtraction) is mapped on the POSIX timestamp that
     represents this non existing instant. *)
  if not (is_date_valid date && is_time_valid t) then None else
  let d = jd_of_date date - jd_posix_epoch in
  let hh_ps = Int64.(mul (of_int hh) ps_count_in_hour) in
  let mm_ps = Int64.(mul (of_int mm) ps_count_in_min) in
  let ss_ps = Int64.(mul (of_int ss) ps_count_in_s) in
  let ps = Int64.(add hh_ps (add mm_ps ss_ps)) in
  sub_span (d, ps) (Span.of_int_s tz_offset_s)

let to_date_time ?(tz_offset_s = 0) t =
  (* To render the timestamp in the given time zone offset we first
     express the timestamp in local time and then compute the date
     fields on that stamp as if it were UTC. If the local timestamp is
     not in [min;max] then its date fields cannot be valid according
     to the constraints guaranteed by Ptime and we fallback to UTC,
     i.e. a time zone offset of 0.

     We then apply the following algorithm whose description makes
     sense on a POSIX timestamp (i.e. UTC) but works equally well to
     render the date-time fields of a local timestamp.

     We first take take the POSIX day count [d] (equivalent by
     definition to an UTC day count) from the epoch, convert it to a
     Julian day and use this to get the proleptic Gregorian calendar
     date. The POSIX picoseconds [ps] in the day are are converted to
     a daytime according to to its various units.

     By definition no POSIX timestamp can represent a date-time with a
     seconds value of 60 (leap second addition) and thus the function
     will never return a date-time with such a value.  On the other
     hand it will return an inexisting UTC date-time with a seconds
     value of 59 whenever a leap second is subtracted since there is a
     POSIX timestamp that represents this instant. *)
  let (d, ps), tz_offset_s = match add_span t (Span.of_int_s tz_offset_s) with
  | None -> t, 0 (* fallback to UTC *)
  | Some local -> local, tz_offset_s
  in
  let jd = d + jd_posix_epoch in
  let date = jd_to_date jd in
  let hh = Int64.(to_int (div ps ps_count_in_hour)) in
  let hh_rem = Int64.rem ps ps_count_in_hour in
  let mm = Int64.(to_int (div hh_rem ps_count_in_min)) in
  let mm_rem = Int64.rem hh_rem ps_count_in_min in
  let ss = Int64.(to_int (div mm_rem ps_count_in_s)) in
  date, ((hh, mm, ss), tz_offset_s)

let of_date ?tz_offset_s:(tz = 0) date = of_date_time (date, ((00, 00, 00), tz))
let to_date ?tz_offset_s t = fst (to_date_time ?tz_offset_s t)
let of_year ?tz_offset_s y = of_date ?tz_offset_s (y, 01, 01)
let to_year ?(tz_offset_s = 0) t =
  let d = match add_span t (Span.of_int_s tz_offset_s) with
  | None -> fst t (* fallback to UTC *) | Some (local_d, _) -> local_d
  in
  jd_to_year (d + jd_posix_epoch)

let weekday_num ?(tz_offset_s = 0) t =
  let (d, _) = Span.add t (Span.of_int_s tz_offset_s) in
  (* N.B. in contrast to [to_date_time] we don't care if we fall outside
    [min;max]. Even if it happens the result of the computation is still
     correct *)
  let i = (d + 4 (* Epoch, d = 0, was a thu, we want 4 for that day *)) mod 7 in
  if i < 0 then 7 + i else i

let weekday =
  let wday = [| `Sun; `Mon; `Tue; `Wed; `Thu; `Fri; `Sat; |] in
  fun ?tz_offset_s t -> wday.(weekday_num ?tz_offset_s t)

(* RFC 3339 timestamp conversions *)

(* RFC 3339 timestamp parser *)

type error_range = int * int
type rfc3339_error =
  [ `Invalid_stamp | `Eoi | `Exp_chars of char list | `Trailing_input ]

let pp_rfc3339_error ppf = function
| `Invalid_stamp -> Format.fprintf ppf "@[invalid@ time@ stamp@]"
| `Eoi -> Format.fprintf ppf "@[unexpected@ end@ of@ input@]"
| `Trailing_input -> Format.fprintf ppf "@[trailing@ input@]"
| `Exp_chars cs ->
    let rec pp_chars ppf = function
    | c :: cs -> Format.fprintf ppf "@ %C" c; pp_chars ppf cs
    | [] -> ()
    in
    Format.fprintf ppf "@[expected@ a@ character@ in:%a@]" pp_chars cs

let pp_range ppf (s, e) =
  if s = e  then Format.pp_print_int ppf s else Format.fprintf ppf "%d-%d" s e

let _rfc3339_error_to_string (r, err) =
  Format.asprintf "@[<h>%a: %a@]" pp_range r pp_rfc3339_error err

let rfc3339_string_error = function
| Ok _ as v -> v | Error (`RFC3339 e) -> Error (_rfc3339_error_to_string e)

let rfc3339_error_to_msg = function
| Ok _ as v -> v | Error (`RFC3339 e) ->
    Error (`Msg (_rfc3339_error_to_string e))

exception RFC3339 of (int * int) * rfc3339_error                  (* Internal *)

let error r e = raise (RFC3339 (r, e))
let error_pos p e = error (p, p) e
let error_exp_digit p =
  error_pos p (`Exp_chars ['0'; '1'; '2'; '3'; '4'; '5'; '6'; '7'; '8'; '9'])

let is_digit = function '0' .. '9' -> true | _ -> false

let parse_digits ~count pos max s =
  let stop = pos + count - 1 in
  if stop > max then error_pos max `Eoi else
  let rec loop k acc =
    if k > stop then acc else
    if is_digit s.[k] then loop (k+1) (acc * 10 + Char.code s.[k] - 0x30) else
    error_exp_digit k
  in
  loop pos 0

let parse_char c pos max s =
  if pos > max then error_pos max `Eoi else
  if s.[pos] = c then () else error_pos pos (`Exp_chars [c])

let parse_dt_sep ~strict pos max s =
  let is_dt_sep = function
  | 'T' -> true
  | 't' | ' ' when not strict -> true
  | _ -> false
  in
  if pos > max then error_pos max `Eoi else
  if is_dt_sep s.[pos] then () else
  error_pos pos (`Exp_chars (['T'] @ if strict then [] else ['t'; ' ']))

let decide_frac_or_tz ~strict pos max s =
  if pos > max then error_pos max `Eoi else
  match s.[pos] with
  | '.' -> `Frac
  | '+' | '-' | 'Z' -> `Tz
  | 'z' when not strict  -> `Tz
  | c ->
      let chars = ['.'; '+'; '-'; 'Z'] @ if strict then [] else ['z'] in
      error_pos pos (`Exp_chars chars)

let parse_frac_ps pos max s =
  if pos > max then error_pos max `Eoi else
  if not (is_digit s.[pos]) then error_exp_digit pos else
  let rec loop k acc pow =
    if k > max then error_pos max `Eoi else
    if not (is_digit s.[k]) then (Some acc), k else
    let count = k - pos + 1 in
    if count > 12 then (* truncate *) loop (k + 1) acc pow else
    let pow = Int64.div pow 10L in
    let acc = Int64.(add acc (mul (of_int (Char.code s.[k] - 0x30)) pow)) in
    loop (k + 1) acc pow
  in
  loop pos 0L ps_count_in_s

let parse_tz_s ~strict pos max s =
  let parse_tz_mag sign pos =
    let hh_pos = pos in
    let hh = parse_digits ~count:2 hh_pos max s in
    let mm, mm_pos = match strict with
    | true ->
        let mm_pos = hh_pos + 3 in
        parse_char ':' (mm_pos - 1) max s;
        parse_digits ~count:2 mm_pos max s, mm_pos
    | false ->
        let next = hh_pos + 2 in
        if next > max || not (s.[next] = ':' || is_digit s.[next])
        then (0, hh_pos (* end pos of parse - 1, one is added at the end *))
        else
        let mm_pos = if s.[next] = ':' then hh_pos + 3 else hh_pos + 2 in
        parse_digits ~count:2 mm_pos max s, mm_pos
    in
    if hh > 23 then error (hh_pos, hh_pos + 1) `Invalid_stamp else
    if mm > 59 then error (mm_pos, mm_pos + 1) `Invalid_stamp else
    let secs = hh * 3600 + mm * 60 in
    let tz_s = match secs = 0 && sign = -1 with
    | true -> None (* -00:00 convention *)
    | false -> Some (sign * secs)
    in
    tz_s, mm_pos + 1
  in
  if pos > max then error_pos max `Eoi else
  match s.[pos] with
  | 'Z' -> Some 0, pos
  | 'z' when not strict -> Some 0, pos
  | '+' -> parse_tz_mag ( 1) (pos + 1)
  | '-' -> parse_tz_mag (-1) (pos + 1)
  | c ->
      let chars = ['+'; '-'; 'Z'] @ if strict then [] else ['z'] in
      error_pos pos (`Exp_chars chars)

let of_rfc3339 ?(strict = false) ?(sub = false) ?(start = 0) s =
  try
    let s_len = String.length s in
    let max = s_len - 1 in
    if s_len = 0 || start < 0 || start > max then error_pos start `Eoi else
    let y_pos = start in
    let m_pos = y_pos + 5 in
    let d_pos = m_pos + 3 in
    let hh_pos = d_pos + 3 in
    let mm_pos = hh_pos + 3 in
    let ss_pos = mm_pos + 3 in
    let decide_pos = ss_pos + 2 in
    let y = parse_digits ~count:4 y_pos max s in
    parse_char '-' (m_pos - 1) max s;
    let m = parse_digits ~count:2 m_pos max s in
    parse_char '-' (d_pos - 1) max s;
    let d = parse_digits ~count:2 d_pos max s in
    parse_dt_sep ~strict (hh_pos - 1) max s;
    let hh =  parse_digits ~count:2 hh_pos max s in
    parse_char ':' (mm_pos - 1) max s;
    let mm =  parse_digits ~count:2 mm_pos max s in
    parse_char ':' (ss_pos - 1) max s;
    let ss = parse_digits ~count:2 ss_pos max s in
    let frac, tz_pos = match decide_frac_or_tz ~strict decide_pos max s with
    | `Frac -> parse_frac_ps (decide_pos + 1) max s
    | `Tz -> None, decide_pos
    in
    let tz_s_opt, last_pos = parse_tz_s ~strict tz_pos max s in
    let tz_s = match tz_s_opt with None -> 0 | Some s -> s in
    match of_date_time ((y, m, d), ((hh, mm, ss), tz_s)) with
    | None -> error (start, last_pos) `Invalid_stamp
    | Some t ->
        let t, tz_s = match frac with
        | None | Some 0L -> t, tz_s
        | Some frac ->
            match add_span t (0, frac) with
            | None -> error (start, last_pos) `Invalid_stamp
            | Some t -> t, tz_s
        in
        if not sub && last_pos <> max
        then error_pos (last_pos + 1) `Trailing_input
        else Ok (t, tz_s_opt, last_pos - start + 1)
  with RFC3339 (r, e) -> Error (`RFC3339 (r, e))

(* RFC 3339 timestamp formatter *)

let rfc3339_adjust_tz_offset tz_offset_s =
  (* The RFC 3339 time zone offset field is limited in expression to
     the bounds below with minute precision. If the requested time
     zone offset exceeds these bounds or is not an *integral* number
     of minutes we simply use UTC. An alternative would be to
     compensate the offset *and* the timestamp but it's more
     complicated to explain and maybe more surprising to the user. *)
  let min = -86340 (* -23h59 in secs *) in
  let max = +86340 (* +23h59 in secs *) in
  if min <= tz_offset_s && tz_offset_s <= max && tz_offset_s mod 60 = 0
  then tz_offset_s, false
  else 0 (* UTC *), true

let s_frac_of_ps frac ps =
  Int64.(div (rem ps ps_count_in_s) Span.frac_div.(frac))

let to_rfc3339 ?(space = false) ?frac_s:(frac = 0) ?tz_offset_s (_, ps as t) =
  let buf = Buffer.create 255 in
  let tz_offset_s, tz_unknown = match tz_offset_s with
  | Some tz -> rfc3339_adjust_tz_offset tz
  | None -> 0, true
  in
  let (y, m, d), ((hh, ss, mm), tz_offset_s) = to_date_time ~tz_offset_s t in
  let dt_sep = if space then ' ' else 'T' in
  Printf.bprintf buf "%04d-%02d-%02d%c%02d:%02d:%02d" y m d dt_sep hh ss mm;
  let frac = if frac < 0 then 0 else (if frac > 12 then 12 else frac) in
  if frac <> 0 then Printf.bprintf buf ".%0*Ld" frac (s_frac_of_ps frac ps);
  if tz_offset_s = 0 && not tz_unknown then Printf.bprintf buf "Z" else
  begin
    let tz_sign = if tz_offset_s < 0 || tz_unknown then '-' else '+' in
    let tz_min = abs (tz_offset_s / 60) in
    let tz_hh = tz_min / 60 in
    let tz_mm = tz_min mod 60 in
    Printf.bprintf buf "%c%02d:%02d" tz_sign tz_hh tz_mm;
  end;
  Buffer.contents buf

let pp_rfc3339 ?space ?frac_s ?tz_offset_s () ppf t =
  Format.fprintf ppf "%s" (to_rfc3339 ?space ?frac_s ?tz_offset_s t)

(* Pretty printing *)

let pp_human ?frac_s:(frac = 0) ?tz_offset_s () ppf (_, ps as t) =
  let tz_offset_s, tz_unknown = match tz_offset_s with
  | Some tz -> rfc3339_adjust_tz_offset tz
  | None -> 0, true
  in
  let (y, m, d), ((hh, ss, mm), tz_offset_s) = to_date_time ~tz_offset_s t in
  Format.fprintf ppf "%04d-%02d-%02d %02d:%02d:%02d" y m d hh ss mm;
  let frac = if frac < 0 then 0 else (if frac > 12 then 12 else frac) in
  if frac <> 0 then Format.fprintf ppf ".%0*Ld" frac (s_frac_of_ps frac ps);
  let tz_sign = if tz_offset_s < 0 || tz_unknown then '-' else '+' in
  let tz_min = abs (tz_offset_s / 60) in
  let tz_hh = tz_min / 60 in
  let tz_mm = tz_min mod 60 in
  Format.fprintf ppf " %c%02d:%02d" tz_sign tz_hh tz_mm;
  ()

let pp = pp_human ~tz_offset_s:0 ()
let dump = Span.dump

(*---------------------------------------------------------------------------
   Copyright (c) 2015 The ptime programmers

   Permission to use, copy, modify, and/or distribute this software for any
   purpose with or without fee is hereby granted, provided that the above
   copyright notice and this permission notice appear in all copies.

   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
   MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
   ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  ---------------------------------------------------------------------------*)