author | wenzelm |
Wed, 12 Mar 2025 11:39:00 +0100 | |
changeset 82265 | 4b875a4c83b0 |
parent 81964 | 8efec8da78f9 |
permissions | -rw-r--r-- |
64056 | 1 |
/* Title: Pure/General/date.scala |
2 |
Author: Makarius |
|
81964 | 3 |
Author: Fabian Huch, TU München |
64056 | 4 |
|
79820 | 5 |
Date and time, with timezone. |
64056 | 6 |
*/ |
7 |
||
8 |
package isabelle |
|
9 |
||
10 |
||
64096 | 11 |
import java.util.Locale |
74943 | 12 |
import java.time.{Instant, ZonedDateTime, LocalTime, ZoneId} |
64056 | 13 |
import java.time.format.{DateTimeFormatter, DateTimeParseException} |
81883
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
14 |
import java.time.temporal.{ChronoUnit, TemporalAccessor} |
64059 | 15 |
|
16 |
import scala.annotation.tailrec |
|
64056 | 17 |
|
18 |
||
75393 | 19 |
object Date { |
64056 | 20 |
/* format */ |
21 |
||
75393 | 22 |
object Format { |
23 |
def make(fmts: List[DateTimeFormatter], tune: String => String = s => s): Format = { |
|
73120
c3589f2dff31
more informative errors: simplify diagnosis of spurious failures reported by users;
wenzelm
parents:
71601
diff
changeset
|
24 |
require(fmts.nonEmpty, "no date formats") |
64059 | 25 |
|
64056 | 26 |
new Format { |
64059 | 27 |
def apply(date: Date): String = fmts.head.format(date.rep) |
28 |
def parse(str: String): Date = |
|
64098 | 29 |
new Date(ZonedDateTime.from(Formatter.try_variants(fmts, tune(str)))) |
64056 | 30 |
} |
64059 | 31 |
} |
64056 | 32 |
|
64098 | 33 |
def apply(pats: String*): Format = |
71601 | 34 |
make(pats.toList.map(Date.Formatter.pattern)) |
64060 | 35 |
|
64098 | 36 |
val default: Format = Format("dd-MMM-uuuu HH:mm:ss xx") |
37 |
val date: Format = Format("dd-MMM-uuuu") |
|
38 |
val time: Format = Format("HH:mm:ss") |
|
73649 | 39 |
val alt_date: Format = Format("uuuuMMdd") |
64056 | 40 |
} |
41 |
||
75393 | 42 |
abstract class Format private { |
64056 | 43 |
def apply(date: Date): String |
44 |
def parse(str: String): Date |
|
64098 | 45 |
|
64056 | 46 |
def unapply(str: String): Option[Date] = |
64098 | 47 |
try { Some(parse(str)) } catch { case _: DateTimeParseException => None } |
64056 | 48 |
} |
64057 | 49 |
|
75393 | 50 |
object Formatter { |
64099 | 51 |
def pattern(pat: String): DateTimeFormatter = DateTimeFormatter.ofPattern(pat) |
52 |
||
53 |
def variants(pats: List[String], locs: List[Locale] = Nil): List[DateTimeFormatter] = |
|
75394 | 54 |
pats.flatMap { pat => |
64099 | 55 |
val fmt = pattern(pat) |
71601 | 56 |
if (locs.isEmpty) List(fmt) else locs.map(fmt.withLocale) |
75394 | 57 |
} |
64099 | 58 |
|
75393 | 59 |
@tailrec def try_variants( |
60 |
fmts: List[DateTimeFormatter], |
|
61 |
str: String, |
|
62 |
last_exn: Option[DateTimeParseException] = None |
|
63 |
): TemporalAccessor = { |
|
64099 | 64 |
fmts match { |
65 |
case Nil => |
|
66 |
throw last_exn.getOrElse(new DateTimeParseException("Failed to parse date", str, 0)) |
|
67 |
case fmt :: rest => |
|
68 |
try { ZonedDateTime.from(fmt.parse(str)) } |
|
69 |
catch { case exn: DateTimeParseException => try_variants(rest, str, Some(exn)) } |
|
70 |
} |
|
71 |
} |
|
72 |
} |
|
73 |
||
64057 | 74 |
|
74925 | 75 |
/* ordering */ |
76 |
||
75393 | 77 |
object Ordering extends scala.math.Ordering[Date] { |
74925 | 78 |
def compare(date1: Date, date2: Date): Int = |
79 |
date1.instant.compareTo(date2.instant) |
|
80 |
} |
|
81 |
||
75393 | 82 |
object Rev_Ordering extends scala.math.Ordering[Date] { |
74925 | 83 |
def compare(date1: Date, date2: Date): Int = |
84 |
date2.instant.compareTo(date1.instant) |
|
85 |
} |
|
86 |
||
87 |
||
64057 | 88 |
/* date operations */ |
89 |
||
69980 | 90 |
def timezone_utc: ZoneId = ZoneId.of("UTC") |
91 |
def timezone_berlin: ZoneId = ZoneId.of("Europe/Berlin") |
|
92 |
||
64057 | 93 |
def timezone(): ZoneId = ZoneId.systemDefault |
94 |
||
79820 | 95 |
def now(timezone: ZoneId = Date.timezone()): Date = |
96 |
new Date(ZonedDateTime.now(timezone)) |
|
64057 | 97 |
|
79820 | 98 |
def instant(t: Instant, timezone: ZoneId = Date.timezone()): Date = |
99 |
new Date(ZonedDateTime.ofInstant(t, timezone)) |
|
65014 | 100 |
|
79820 | 101 |
def apply(t: Time, timezone: ZoneId = Date.timezone()): Date = |
102 |
instant(t.instant, timezone) |
|
81883
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
103 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
104 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
105 |
/* varying-length calendar cycles */ |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
106 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
107 |
enum Day { case mon, tue, wed, thu, fri, sat, sun } |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
108 |
enum Month { case jan, feb, mar, apr, may, jun, jul, aug, sep, okt, nov, dec } |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
109 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
110 |
sealed trait Cycle { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
111 |
def zero(date: Date): Date |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
112 |
def next(date: Date): Date |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
113 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
114 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
115 |
case class Daily(at: Time = Time.zero) extends Cycle { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
116 |
require(at >= Time.zero && at < Time.hms(24, 0, 0)) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
117 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
118 |
def zero(date: Date): Date = date.midnight |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
119 |
def next(date: Date): Date = { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
120 |
val start = zero(date) + at |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
121 |
if (date.time < start.time) start else start.shift(1) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
122 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
123 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
124 |
override def toString: String = "Daily(" + Format.time(Date(at, timezone_utc)) + ")" |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
125 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
126 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
127 |
case class Weekly(on: Day = Day.mon, step: Daily = Daily()) extends Cycle { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
128 |
def zero(date: Date): Date = date.shift(1 - date.rep.getDayOfWeek.getValue).midnight |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
129 |
def next(date: Date): Date = { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
130 |
val next = step.next(zero(date).shift(on.ordinal) - Time.ms(1)) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
131 |
if (date.time < next.time) next else Date(next.rep.plus(1, ChronoUnit.WEEKS)) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
132 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
133 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
134 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
135 |
case class Monthly(nth: Int = 1, step: Daily = Daily()) extends Cycle { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
136 |
require(nth > 0 && nth <= 31) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
137 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
138 |
def zero(date: Date): Date = date.shift(1 - date.rep.getDayOfMonth).midnight |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
139 |
def next(date: Date): Date = { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
140 |
@tailrec def find_next(zero: Date): Date = { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
141 |
val next = step.next(zero.shift(nth - 1) - Time.ms(1)) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
142 |
if (next.rep.getDayOfMonth == nth && date.time < next.time) next |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
143 |
else find_next(Date(zero.rep.plus(1, ChronoUnit.MONTHS))) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
144 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
145 |
find_next(zero(date)) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
146 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
147 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
148 |
|
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
149 |
case class Yearly(in: Month = Month.jan, step: Monthly = Monthly()) extends Cycle { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
150 |
def zero(date: Date): Date = date.shift(1 - date.rep.getDayOfYear).midnight |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
151 |
def next(date: Date): Date = { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
152 |
@tailrec def find_next(zero: Date): Date = { |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
153 |
val next = step.next(Date(zero.rep.plus(in.ordinal, ChronoUnit.MONTHS)) - Time.ms(1)) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
154 |
if (next.rep.getMonthValue - 1 == in.ordinal && date.time < next.time) next |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
155 |
else find_next(Date(zero.rep.plus(1, ChronoUnit.YEARS))) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
156 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
157 |
find_next(zero(date)) |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
158 |
} |
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
159 |
} |
64056 | 160 |
} |
161 |
||
75393 | 162 |
sealed case class Date(rep: ZonedDateTime) { |
81883
7ce4a45fe4c2
varying-length calendar cycles, e.g. for ci jobs every week on a certain day/time;
Fabian Huch <huch@in.tum.de>
parents:
79820
diff
changeset
|
163 |
def shift(days: Int): Date = Date(rep.plus(days, ChronoUnit.DAYS)) |
64117 | 164 |
def midnight: Date = |
165 |
new Date(ZonedDateTime.of(rep.toLocalDate, LocalTime.MIDNIGHT, rep.getZone)) |
|
166 |
||
79820 | 167 |
def to(other: ZoneId): Date = new Date(rep.withZoneSameInstant(other)) |
64111 | 168 |
|
65733 | 169 |
def unix_epoch: Long = rep.toEpochSecond |
77163
7ceed24c88dc
alternate AFP tests on lrzcloud2, to fit better into one day;
wenzelm
parents:
75394
diff
changeset
|
170 |
def unix_epoch_day: Long = rep.toLocalDate.toEpochDay |
65014 | 171 |
def instant: Instant = Instant.from(rep) |
172 |
def time: Time = Time.instant(instant) |
|
64057 | 173 |
def timezone: ZoneId = rep.getZone |
174 |
||
79820 | 175 |
def + (t: Time): Date = Date(time + t, timezone = timezone) |
176 |
def - (t: Time): Date = Date(time - t, timezone = timezone) |
|
79817 | 177 |
def - (d: Date): Time = time - d.time |
178 |
||
64057 | 179 |
def format(fmt: Date.Format = Date.Format.default): String = fmt(this) |
180 |
override def toString: String = format() |
|
64056 | 181 |
} |