0 | module IotaTime.Calendar
2 | import public IotaTime.Calendar.Component
3 | import IotaTime.Period
4 | import Derive.Prelude
6 | %language ElabReflection
14 | = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday
18 | weekdayNumber : DayOfWeek -> Integer
19 | weekdayNumber Sunday = 0
20 | weekdayNumber Monday = 1
21 | weekdayNumber Tuesday = 2
22 | weekdayNumber Wednesday = 3
23 | weekdayNumber Thursday = 4
24 | weekdayNumber Friday = 5
25 | weekdayNumber Saturday = 6
29 | daysPerWeek : Integer
35 | weekdayFromNumber : Integer -> DayOfWeek
36 | weekdayFromNumber value = case value `mod` 7 of
47 | nextWeekdayOffset : Integer -> DayOfWeek -> DayOfWeek -> Integer
48 | nextWeekdayOffset count current target =
49 | let currentNumber = weekdayNumber current
50 | targetNumber = weekdayNumber target
51 | weeks = if targetNumber > currentNumber then count - 1 else count
52 | in daysPerWeek * weeks + targetNumber - currentNumber
56 | previousWeekdayOffset : Integer -> DayOfWeek -> DayOfWeek -> Integer
57 | previousWeekdayOffset count current target =
58 | let currentNumber = weekdayNumber current
59 | targetNumber = weekdayNumber target
60 | weeks = if targetNumber < currentNumber then count - 1 else count
61 | in -(daysPerWeek * weeks + currentNumber - targetNumber)
66 | applyDatePeriodWith :
67 | (shiftYears : Integer -> date -> date) ->
68 | (shiftMonths : Integer -> date -> date) ->
69 | (shiftDays : Integer -> date -> date) ->
70 | Period target -> date -> date
71 | applyDatePeriodWith shiftYears shiftMonths shiftDays period =
72 | shiftDays (periodDays period)
73 | . shiftDays (daysPerWeek * periodWeeks period)
74 | . shiftMonths (periodMonths period)
75 | . shiftYears (periodYears period)
79 | left == right = weekdayNumber left == weekdayNumber right
83 | compare left right = compare (weekdayNumber left) (weekdayNumber right)
85 | %runElab derive `{DayOfWeek
} [Show]
100 | %runElab derive `{DayNth
} [Eq, Show]
106 | nthWeekdayDayNumber : DayNth -> (monthLength : Integer) ->
107 | (firstOffset : Integer) -> (lastOffset : Integer) ->
109 | nthWeekdayDayNumber FourthToLast monthLength _ lastOffset =
110 | monthLength - lastOffset - 3 * daysPerWeek
111 | nthWeekdayDayNumber ThirdToLast monthLength _ lastOffset =
112 | monthLength - lastOffset - 2 * daysPerWeek
113 | nthWeekdayDayNumber SecondToLast monthLength _ lastOffset =
114 | monthLength - lastOffset - daysPerWeek
115 | nthWeekdayDayNumber Last monthLength _ lastOffset =
116 | monthLength - lastOffset
117 | nthWeekdayDayNumber First _ firstOffset _ = 1 + firstOffset
118 | nthWeekdayDayNumber Second _ firstOffset _ =
119 | 1 + daysPerWeek + firstOffset
120 | nthWeekdayDayNumber Third _ firstOffset _ =
121 | 1 + 2 * daysPerWeek + firstOffset
122 | nthWeekdayDayNumber Fourth _ firstOffset _ =
123 | 1 + 3 * daysPerWeek + firstOffset
124 | nthWeekdayDayNumber Fifth _ firstOffset _ =
125 | 1 + 4 * daysPerWeek + firstOffset
130 | data CalendarConversionError = TargetCalendarOutOfRange String Integer
134 | data DateDifferenceUnits = DaysOnly | YearsMonthsDays
138 | data MonthArithmeticPolicy = ClampToMonth
142 | record DateDifferencePolicy where
143 | constructor MkDateDifferencePolicy
144 | units : DateDifferenceUnits
145 | monthArithmetic : MonthArithmeticPolicy
149 | nodaTimePolicy : DateDifferencePolicy
150 | nodaTimePolicy = MkDateDifferencePolicy YearsMonthsDays ClampToMonth
156 | interface Calendar calendar where
158 | MonthRep : Year -> Type
160 | isValidDays : Integer -> Bool
161 | fromDays : (days : Integer) -> {auto 0 valid : So (isValidDays days)} -> DateRep
162 | toDaysFor : DateRep -> Integer
163 | 0 toDaysValid : (date : DateRep) -> So (isValidDays (toDaysFor date))
164 | 0 toFromDays : (days : Integer) -> (0 valid : So (isValidDays days)) ->
165 | toDaysFor (fromDays days {valid}) = days
166 | 0 fromToDays : (date : DateRep) ->
167 | fromDays (toDaysFor date) {valid = toDaysValid date} = date
168 | calendarName : String
170 | year' : DateRep -> Year
171 | toYmd : (date : DateRep) -> (MonthRep (year' date), DayOfMonth)
172 | day' : DateRep -> DayOfMonth
173 | month' : (date : DateRep) -> MonthRep (year' date)
175 | applyCalendarPeriod' : Period target -> DateRep -> DateRep
176 | shiftCalendarDays' : Integer -> DateRep -> DateRep
178 | dayOfWeekFor : DateRep -> DayOfWeek
179 | nextFor : Integer -> DayOfWeek -> DateRep -> DateRep
180 | previousFor : Integer -> DayOfWeek -> DateRep -> DateRep
184 | CalendarDate : (calendar : Type) -> {auto cal : Calendar calendar} -> Type
185 | CalendarDate calendar @{cal} = DateRep @{cal}
191 | interface HasCalendarBridge date where
192 | toBridgeDays : date -> Integer
193 | acceptsBridgeDays : Integer -> Bool
194 | fromBridgeDays : (days : Integer) ->
195 | {auto 0 valid : So (acceptsBridgeDays days)} -> date
196 | bridgeCalendarName : String
202 | interface HasCalendarBridge date => CalendarValue date where
203 | CalendarMonth : Year -> Type
204 | calendarValueToDays : date -> Integer
205 | calendarValueYear : date -> Year
206 | calendarValueMonthDay : (value : date) ->
207 | (CalendarMonth (calendarValueYear value), DayOfMonth)
208 | calendarValueDayOfWeek : date -> DayOfWeek
209 | calendarValueBetweenWith :
210 | DateDifferencePolicy -> date -> date -> Period date
214 | interface CalendarValue date => CalendarNavigation date where
215 | calendarValueNext : Integer -> DayOfWeek -> date -> date
216 | calendarValuePrevious : Integer -> DayOfWeek -> date -> date
220 | yearFor : {calendar : Type} -> {auto cal : Calendar calendar} ->
221 | CalendarDate calendar @{cal} -> Year
222 | yearFor @{cal} = year' @{cal}
226 | monthFor : {calendar : Type} -> {auto cal : Calendar calendar} ->
227 | (date : CalendarDate calendar @{cal}) ->
228 | MonthRep @{cal} (yearFor {calendar} @{cal} date)
229 | monthFor @{cal} = month' @{cal}
233 | dayFor : {calendar : Type} -> {auto cal : Calendar calendar} ->
234 | CalendarDate calendar @{cal} -> DayOfMonth
235 | dayFor @{cal} = day' @{cal}
238 | applyCalendarPeriod : {calendar : Type} -> {auto cal : Calendar calendar} ->
239 | Period target -> CalendarDate calendar @{cal} ->
240 | CalendarDate calendar @{cal}
241 | applyCalendarPeriod @{cal} = applyCalendarPeriod' @{cal}
244 | shiftCalendarDays : {calendar : Type} -> {auto cal : Calendar calendar} ->
245 | Integer -> CalendarDate calendar @{cal} -> CalendarDate calendar @{cal}
246 | shiftCalendarDays @{cal} = shiftCalendarDays' @{cal}
250 | betweenDaysFor : {calendar : Type} -> {auto cal : Calendar calendar} ->
251 | {auto target : HasCalendar (CalendarDate calendar @{cal})} ->
252 | (start : CalendarDate calendar @{cal}) ->
253 | (end : CalendarDate calendar @{cal}) ->
254 | Period (CalendarDate calendar @{cal})
255 | betweenDaysFor @{cal} start end = days (toDaysFor @{cal} end - toDaysFor @{cal} start)
257 | yearsBetween : {calendar : Type} -> {auto cal : Calendar calendar} ->
258 | {auto target : HasCalendar (CalendarDate calendar @{cal})} ->
259 | CalendarDate calendar @{cal} -> CalendarDate calendar @{cal} -> Integer
260 | yearsBetween @{cal} start end =
261 | let estimate = yearValue (yearFor @{cal} end) - yearValue (yearFor @{cal} start)
262 | estimatedDate = applyCalendarPeriod @{cal}
263 | (years {target = CalendarDate calendar @{cal}} estimate) start
264 | estimatedDays = toDaysFor @{cal} estimatedDate
265 | startDays = toDaysFor @{cal} start
266 | endDays = toDaysFor @{cal} end
267 | in if startDays <= endDays
268 | then if estimatedDays <= endDays then estimate else estimate - 1
269 | else if estimatedDays >= endDays then estimate else estimate + 1
271 | monthsBetween : {calendar : Type} -> {auto cal : Calendar calendar} ->
272 | {auto target : HasCalendar (CalendarDate calendar @{cal})} ->
273 | CalendarDate calendar @{cal} -> CalendarDate calendar @{cal} -> Integer
274 | monthsBetween @{cal} start end =
275 | let startDays = toDaysFor @{cal} start
276 | endDays = toDaysFor @{cal} end
277 | fuel = cast (abs (endDays - startDays) + 1)
278 | in if startDays <= endDays
279 | then forward fuel 0
280 | else backward fuel 0
282 | forward : Nat -> Integer -> Integer
283 | forward Z count = count
284 | forward (S fuel) count =
285 | let candidate = count + 1
286 | candidateDays = toDaysFor @{cal}
287 | (applyCalendarPeriod @{cal}
288 | (months {target = CalendarDate calendar @{cal}} candidate) start)
289 | in if candidateDays <= toDaysFor @{cal} end
290 | then if candidateDays == toDaysFor @{cal} end
292 | else forward fuel candidate
295 | backward : Nat -> Integer -> Integer
296 | backward Z count = count
297 | backward (S fuel) count =
298 | let candidate = count - 1
299 | candidateDays = toDaysFor @{cal}
300 | (applyCalendarPeriod @{cal}
301 | (months {target = CalendarDate calendar @{cal}} candidate) start)
302 | in if candidateDays >= toDaysFor @{cal} end
303 | then if candidateDays == toDaysFor @{cal} end
305 | else backward fuel candidate
311 | betweenWithFor : {calendar : Type} -> {auto cal : Calendar calendar} ->
312 | {auto target : HasCalendar (CalendarDate calendar @{cal})} ->
313 | DateDifferencePolicy ->
314 | (start : CalendarDate calendar @{cal}) ->
315 | (end : CalendarDate calendar @{cal}) ->
316 | Period (CalendarDate calendar @{cal})
317 | betweenWithFor @{cal} (MkDateDifferencePolicy DaysOnly _) start end =
318 | betweenDaysFor @{cal} start end
319 | betweenWithFor @{cal} (MkDateDifferencePolicy YearsMonthsDays ClampToMonth) start end =
320 | let yearCount = yearsBetween @{cal} start end
321 | afterYears = applyCalendarPeriod @{cal}
322 | (years {target = CalendarDate calendar @{cal}} yearCount) start
323 | monthCount = monthsBetween @{cal} afterYears end
324 | afterMonths = applyCalendarPeriod @{cal}
325 | (months {target = CalendarDate calendar @{cal}} monthCount) afterYears
326 | dayCount = toDaysFor @{cal} end - toDaysFor @{cal} afterMonths
327 | in years {target = CalendarDate calendar @{cal}} yearCount <+>
328 | months {target = CalendarDate calendar @{cal}} monthCount <+>
329 | days {target = CalendarDate calendar @{cal}} dayCount
333 | betweenFor : {calendar : Type} -> {auto cal : Calendar calendar} ->
334 | {auto target : HasCalendar (CalendarDate calendar @{cal})} ->
335 | (start : CalendarDate calendar @{cal}) ->
336 | (end : CalendarDate calendar @{cal}) ->
337 | Period (CalendarDate calendar @{cal})
338 | betweenFor @{cal} = betweenWithFor @{cal} nodaTimePolicy
342 | yearMonthDayFor : {calendar : Type} -> {auto cal : Calendar calendar} ->
343 | (date : CalendarDate calendar @{cal}) ->
344 | (valueYear : Year ** (MonthRep @{cal} valueYear, DayOfMonth))
345 | yearMonthDayFor @{cal} date =
346 | (
year' @{cal} date ** toYmd @{cal} date)
350 | betweenDays : (start : date) -> {auto value : CalendarValue date} ->
351 | date -> Period date
352 | betweenDays start @{value} =
353 | calendarValueBetweenWith @{value}
354 | (MkDateDifferencePolicy DaysOnly ClampToMonth) start
359 | betweenWith : DateDifferencePolicy ->
360 | (start : date) -> {auto value : CalendarValue date} ->
361 | date -> Period date
362 | betweenWith policy start @{value} =
363 | calendarValueBetweenWith @{value} policy start
367 | between : (start : date) -> {auto value : CalendarValue date} ->
368 | date -> Period date
369 | between = betweenWith nodaTimePolicy
373 | yearMonthDay : (value : date) -> {auto rep : CalendarValue date} ->
374 | (valueYear : Year **
375 | (CalendarMonth @{rep} valueYear, DayOfMonth))
376 | yearMonthDay value @{rep} =
377 | (
calendarValueYear @{rep} value ** calendarValueMonthDay @{rep} value)
381 | toDays : (value : date) -> {auto rep : CalendarValue date} -> Integer
382 | toDays value @{rep} = calendarValueToDays @{rep} value
386 | year : (value : date) -> {auto rep : CalendarValue date} -> Year
387 | year value @{rep} = calendarValueYear @{rep} value
391 | month : (value : date) -> {auto rep : CalendarValue date} ->
392 | CalendarMonth @{rep} (year value @{rep})
393 | month value @{rep} = fst (calendarValueMonthDay @{rep} value)
397 | day : (value : date) -> {auto rep : CalendarValue date} -> DayOfMonth
398 | day value @{rep} = snd (calendarValueMonthDay @{rep} value)
402 | calendarComponentsCoherent :
403 | (value : date) -> {auto rep : CalendarValue date} ->
404 | yearMonthDay value @{rep} =
405 | (
year value @{rep} ** (month value @{rep}, day value @{rep}))
406 | calendarComponentsCoherent value @{rep} with
407 | (calendarValueMonthDay @{rep} value)
412 | dayOfWeek : (value : date) -> {auto rep : CalendarValue date} -> DayOfWeek
413 | dayOfWeek value @{rep} = calendarValueDayOfWeek @{rep} value
417 | next : {auto navigation : CalendarNavigation date} ->
418 | Integer -> DayOfWeek -> (value : date) -> date
419 | next count weekday value @{navigation} =
420 | calendarValueNext @{navigation} count weekday value
424 | previous : {auto navigation : CalendarNavigation date} ->
425 | Integer -> DayOfWeek -> (value : date) -> date
426 | previous count weekday value @{navigation} =
427 | calendarValuePrevious @{navigation} count weekday value
432 | withCalendar : {sourceDate : Type} -> {targetDate : Type} ->
433 | {auto sourceRep : HasCalendarBridge sourceDate} ->
434 | {auto targetRep : HasCalendarBridge targetDate} ->
436 | Either CalendarConversionError targetDate
437 | withCalendar @{sourceRep} @{targetRep} date =
438 | let valueDays = toBridgeDays @{sourceRep} date
439 | in case choose (acceptsBridgeDays @{targetRep} valueDays) of
440 | Left valid => Right (fromBridgeDays @{targetRep} valueDays @{valid})
442 | (TargetCalendarOutOfRange (bridgeCalendarName @{targetRep}) valueDays)