0 | module IotaTime.Calendar.Persian
2 | import IotaTime.Internal.ApplyPeriod
3 | import IotaTime.Calendar
4 | import IotaTime.Period
6 | import Derive.Prelude
8 | %language ElabReflection
14 | data Persian = PersianCalendar
16 | namespace PersianMonths
19 | = Farvardin | Ordibehesht | Khordad | Tir | Mordad | Shahrivar
20 | | Mehr | Aban | Azar | Dey | Bahman | Esfand
23 | monthNumber : PersianMonth -> Integer
24 | monthNumber Farvardin = 1
25 | monthNumber Ordibehesht = 2
26 | monthNumber Khordad = 3
28 | monthNumber Mordad = 5
29 | monthNumber Shahrivar = 6
30 | monthNumber Mehr = 7
31 | monthNumber Aban = 8
32 | monthNumber Azar = 9
33 | monthNumber Dey = 10
34 | monthNumber Bahman = 11
35 | monthNumber Esfand = 12
38 | Eq PersianMonth where
39 | left == right = monthNumber left == monthNumber right
42 | Ord PersianMonth where
43 | compare left right = compare (monthNumber left) (monthNumber right)
45 | %runElab derive `{PersianMonth
} [Show]
47 | monthFromNumber : Integer -> PersianMonth
48 | monthFromNumber 1 = PersianMonths.Farvardin
49 | monthFromNumber 2 = PersianMonths.Ordibehesht
50 | monthFromNumber 3 = PersianMonths.Khordad
51 | monthFromNumber 4 = PersianMonths.Tir
52 | monthFromNumber 5 = PersianMonths.Mordad
53 | monthFromNumber 6 = PersianMonths.Shahrivar
54 | monthFromNumber 7 = PersianMonths.Mehr
55 | monthFromNumber 8 = PersianMonths.Aban
56 | monthFromNumber 9 = PersianMonths.Azar
57 | monthFromNumber 10 = PersianMonths.Dey
58 | monthFromNumber 11 = PersianMonths.Bahman
59 | monthFromNumber _ = PersianMonths.Esfand
62 | weekdayFromDays : Integer -> DayOfWeek
63 | weekdayFromDays value = weekdayFromNumber (value + 3)
66 | minimumYear : Integer
70 | maximumYear : Integer
78 | leapYears : List Integer
80 | [ 5, 9, 13, 17, 21, 25, 29, 33, 38, 42, 46, 50, 54, 58, 62, 66
81 | , 71, 75, 79, 83, 87, 91, 95, 99, 104, 108, 112, 116, 120, 124, 128, 132
82 | , 137, 141, 145, 149, 153, 157, 161, 166, 170, 174, 178, 182, 186, 190, 194
83 | , 199, 203, 207, 211, 215, 219, 223, 227, 232, 236, 240, 244, 248, 252, 256
84 | , 260, 265, 269, 273, 277, 281, 285, 289, 293, 298, 302, 306, 310, 314, 318
85 | , 322, 326, 331, 335, 339, 343, 347, 351, 355, 359, 364, 368, 372, 376, 380
86 | , 384, 388, 392, 397, 401, 405, 409, 413, 417, 421, 426, 430, 434, 438, 442
87 | , 446, 450, 454, 459, 463, 467, 471, 475, 479, 483, 487, 492, 496, 500, 504
88 | , 508, 512, 516, 520, 525, 529, 533, 537, 541, 545, 549, 553, 558, 562, 566
89 | , 570, 574, 578, 582, 586, 591, 595, 599, 603, 607, 611, 615, 619, 624, 628
90 | , 632, 636, 640, 644, 648, 652, 657, 661, 665, 669, 673, 677, 681, 686, 690
91 | , 694, 698, 702, 706, 710, 714, 719, 723, 727, 731, 735, 739, 743, 747, 752
92 | , 756, 760, 764, 768, 772, 776, 780, 784, 789, 793, 797, 801, 805, 809, 813
93 | , 818, 822, 826, 830, 834, 838, 842, 846, 851, 855, 859, 863, 867, 871, 875
94 | , 879, 884, 888, 892, 896, 900, 904, 908, 912, 917, 921, 925, 929, 933, 937
95 | , 941, 945, 950, 954, 958, 962, 966, 970, 974, 978, 983, 987, 991, 995, 999
96 | , 1003, 1007, 1011, 1016, 1020, 1024, 1028, 1032, 1036, 1040, 1044, 1049
97 | , 1053, 1057, 1061, 1065, 1069, 1073, 1077, 1082, 1086, 1090, 1094, 1098
98 | , 1102, 1106, 1111, 1115, 1119, 1123, 1127, 1131, 1135, 1139, 1144, 1148
99 | , 1152, 1156, 1160, 1164, 1168, 1172, 1176, 1181, 1185, 1189, 1193, 1197
100 | , 1201, 1205, 1210, 1214, 1218, 1222, 1226, 1230, 1234, 1238, 1243, 1247
101 | , 1251, 1255, 1259, 1263, 1267, 1271, 1276, 1280, 1284, 1288, 1292, 1296
102 | , 1300, 1304, 1309, 1313, 1317, 1321, 1325, 1329, 1333, 1337, 1342, 1346
103 | , 1350, 1354, 1358, 1362, 1366, 1370, 1375, 1379, 1383, 1387, 1391, 1395
104 | , 1399, 1403, 1408, 1412, 1416, 1420, 1424, 1428, 1432, 1436, 1441, 1445
105 | , 1449, 1453, 1457, 1461, 1465, 1469, 1474, 1478, 1482, 1486, 1490, 1494
111 | isLeapYear : Year -> Bool
112 | isLeapYear value = elem (yearValue value) leapYears
115 | countLeapsBefore : Integer -> List Integer -> Integer
116 | countLeapsBefore _ [] = 0
117 | countLeapsBefore year (leapYear :: rest) =
118 | if leapYear < year then 1 + countLeapsBefore year rest else 0
121 | newYearDay : Year -> Integer
123 | epoch + (yearValue value - 1) * 365 +
124 | countLeapsBefore (yearValue value) leapYears
128 | lastDay = newYearDay 1501 - 1
131 | record PersianDate where
132 | constructor MkPersianDate
133 | daysSinceEpoch : Integer
135 | (daysSinceEpoch >= IotaTime.Calendar.Persian.epoch &&
136 | daysSinceEpoch <= IotaTime.Calendar.Persian.lastDay)
139 | Eq PersianDate where
140 | left == right = left.daysSinceEpoch == right.daysSinceEpoch
143 | Ord PersianDate where
144 | compare left right = compare left.daysSinceEpoch right.daysSinceEpoch
146 | checkedPersianDate : (days : Integer) ->
148 | (days >= IotaTime.Calendar.Persian.epoch &&
149 | days <= IotaTime.Calendar.Persian.lastDay)) ->
151 | checkedPersianDate days valid = MkPersianDate days valid
154 | maxDaysInMonth : PersianMonth -> Year -> DayOfMonth
155 | maxDaysInMonth PersianMonths.Esfand value =
156 | if isLeapYear value then 30 else 29
157 | maxDaysInMonth valueMonth _ =
158 | if PersianMonths.monthNumber valueMonth <= 6 then 31 else 30
161 | isValidDate : DayOfMonth -> PersianMonth -> Year -> Bool
162 | isValidDate valueDay valueMonth valueYear =
163 | let dayNumber = dayOfMonthValue valueDay
164 | yearNumber = yearValue valueYear
165 | maxDay = dayOfMonthValue (maxDaysInMonth valueMonth valueYear)
166 | in dayNumber >= 1 && dayNumber <= maxDay &&
167 | yearNumber >= minimumYear && yearNumber <= maximumYear
170 | monthOffset : PersianMonth -> Integer
171 | monthOffset value =
172 | let number = PersianMonths.monthNumber value
173 | in if number <= 6 then (number - 1) * 31
174 | else 186 + (number - 7) * 30
177 | daysFromCivil : Year -> PersianMonth -> DayOfMonth -> Integer
178 | daysFromCivil valueYear valueMonth valueDay =
179 | newYearDay valueYear + monthOffset valueMonth +
180 | dayOfMonthValue valueDay - 1
182 | findPersianYear : Nat -> Integer -> Integer -> Integer
183 | findPersianYear Z estimate days = estimate
184 | findPersianYear (S fuel) estimate days =
185 | if days < newYearDay (yearFromInteger estimate)
186 | then findPersianYear fuel (estimate - 1) days
187 | else if days >= newYearDay (yearFromInteger (estimate + 1))
188 | then findPersianYear fuel (estimate + 1) days
191 | persianCivilFromDays : Integer -> (Year, PersianMonth, DayOfMonth)
192 | persianCivilFromDays value =
193 | let estimate = max minimumYear (min maximumYear
194 | ((value - epoch) `div` 365 + 1))
195 | yearNumber = findPersianYear 1500 estimate value
196 | valueYear = yearFromInteger yearNumber
197 | dayOfYear = value - newYearDay valueYear
198 | monthNumber = if dayOfYear == 365 then 12
199 | else if dayOfYear < 186 then dayOfYear `div` 31 + 1
200 | else (dayOfYear - 186) `div` 30 + 7
201 | offset = if monthNumber <= 6 then (monthNumber - 1) * 31
202 | else 186 + (monthNumber - 7) * 30
203 | dayNumber = dayOfYear - offset + 1
204 | in (valueYear, monthFromNumber monthNumber,
205 | dayOfMonthFromInteger dayNumber)
208 | HasCalendarBridge PersianDate where
209 | toBridgeDays = daysSinceEpoch
210 | acceptsBridgeDays value = value >= epoch && value <= lastDay
211 | fromBridgeDays days @{valid} = checkedPersianDate days valid
212 | bridgeCalendarName = "Persian"
214 | clampToPersian : Integer -> Integer
215 | clampToPersian = max epoch . min lastDay
217 | makePersianDate : Integer -> PersianDate
218 | makePersianDate days =
219 | let clamped = clampToPersian days
220 | in case choose (clamped >= epoch && clamped <= lastDay) of
221 | Left valid => checkedPersianDate clamped valid
222 | Right _ => checkedPersianDate epoch Oh
224 | shiftPersianDays : Integer -> PersianDate -> PersianDate
225 | shiftPersianDays amount date =
226 | makePersianDate (date.daysSinceEpoch + amount)
228 | shiftPersianMonths : Integer -> PersianDate -> PersianDate
229 | shiftPersianMonths amount date =
230 | let (valueYear, valueMonth, valueDay) =
231 | persianCivilFromDays date.daysSinceEpoch
232 | monthOrdinal = PersianMonths.monthNumber valueMonth - 1 + amount
233 | targetYear = yearFromInteger (yearValue valueYear + monthOrdinal `div` 12)
234 | targetMonth = monthFromNumber (monthOrdinal `mod` 12 + 1)
235 | targetDay = min valueDay (maxDaysInMonth targetMonth targetYear)
236 | in makePersianDate (daysFromCivil targetYear targetMonth targetDay)
238 | shiftPersianYears : Integer -> PersianDate -> PersianDate
239 | shiftPersianYears amount date =
240 | let (valueYear, valueMonth, valueDay) =
241 | persianCivilFromDays date.daysSinceEpoch
242 | targetYear = yearFromInteger (yearValue valueYear + amount)
243 | targetDay = min valueDay (maxDaysInMonth valueMonth targetYear)
244 | in makePersianDate (daysFromCivil targetYear valueMonth targetDay)
246 | applyPersianPeriod : Period target -> PersianDate -> PersianDate
247 | applyPersianPeriod = applyDatePeriodWith
248 | shiftPersianYears shiftPersianMonths shiftPersianDays
250 | persianDayOfWeek : PersianDate -> DayOfWeek
251 | persianDayOfWeek date = weekdayFromDays date.daysSinceEpoch
253 | nextPersian : Integer -> DayOfWeek -> PersianDate -> PersianDate
254 | nextPersian count target date =
255 | makePersianDate (date.daysSinceEpoch +
256 | nextWeekdayOffset count (persianDayOfWeek date) target)
258 | previousPersian : Integer -> DayOfWeek -> PersianDate -> PersianDate
259 | previousPersian count target date =
260 | makePersianDate (date.daysSinceEpoch +
261 | previousWeekdayOffset count (persianDayOfWeek date) target)
264 | Calendar Persian where
265 | DateRep = PersianDate
266 | MonthRep _ = PersianMonth
268 | isValidDays value = value >= epoch && value <= lastDay
269 | fromDays days @{valid} = checkedPersianDate days valid
270 | toDaysFor date = date.daysSinceEpoch
271 | toDaysValid (MkPersianDate _ valid) = valid
272 | toFromDays _ _ = Refl
273 | fromToDays (MkPersianDate _ _) = Refl
274 | calendarName = "Persian"
276 | year' date = let (value, _, _) = persianCivilFromDays date.daysSinceEpoch in value
277 | toYmd date = let (_, valueMonth, valueDay) =
278 | persianCivilFromDays date.daysSinceEpoch
279 | in (valueMonth, valueDay)
280 | day' date = let (_, _, value) = persianCivilFromDays date.daysSinceEpoch in value
281 | month' date = let (_, value, _) = persianCivilFromDays date.daysSinceEpoch in value
283 | applyCalendarPeriod' = applyPersianPeriod
284 | shiftCalendarDays' = shiftPersianDays
286 | dayOfWeekFor = persianDayOfWeek
287 | nextFor = nextPersian
288 | previousFor = previousPersian
291 | Show PersianDate where
292 | show date = case persianCivilFromDays date.daysSinceEpoch of
293 | (valueYear, valueMonth, valueDay) =>
294 | "calendarDate " ++ show valueDay ++ " " ++
295 | show valueMonth ++ " " ++ show valueYear
298 | HasCalendar PersianDate where
299 | calendarCapability = ()
302 | PeriodTarget PersianDate where
306 | ApplyPeriod PersianDate where
307 | applyPeriod = applyPersianPeriod
310 | CalendarValue PersianDate where
311 | CalendarMonth _ = PersianMonth
312 | calendarValueToDays = toDaysFor {calendar = Persian}
313 | calendarValueYear = yearFor {calendar = Persian}
314 | calendarValueMonthDay = toYmd {calendar = Persian}
315 | calendarValueDayOfWeek = dayOfWeekFor {calendar = Persian}
316 | calendarValueBetweenWith = betweenWithFor {calendar = Persian}
319 | CalendarNavigation PersianDate where
320 | calendarValueNext = nextFor {calendar = Persian}
321 | calendarValuePrevious = previousFor {calendar = Persian}
325 | calendarDate : (valueDay : DayOfMonth) -> (valueMonth : PersianMonth) ->
326 | (valueYear : Year) ->
328 | (isValidDate valueDay valueMonth valueYear)} ->
329 | CalendarDate Persian
330 | calendarDate valueDay valueMonth valueYear =
331 | makePersianDate (daysFromCivil valueYear valueMonth valueDay)
335 | data PersianDateError
336 | = InvalidPersianDate DayOfMonth PersianMonth Year
337 | | InvalidPersianDayCount Integer
338 | | InvalidPersianNthDay DayNth DayOfWeek PersianMonth Year
339 | | InvalidPersianWeekDate WeekNumber DayOfWeek Year
343 | refineDate : DayOfMonth -> PersianMonth -> Year ->
344 | Either PersianDateError (CalendarDate Persian)
345 | refineDate valueDay valueMonth valueYear =
346 | case choose (isValidDate valueDay valueMonth valueYear) of
347 | Left valid => Right (calendarDate valueDay valueMonth valueYear @{valid})
348 | Right _ => Left (InvalidPersianDate valueDay valueMonth valueYear)
352 | fromDays : (days : Integer) ->
354 | (IotaTime.Calendar.isValidDays {calendar = Persian} days)} ->
355 | CalendarDate Persian
356 | fromDays days @{valid} = checkedPersianDate days valid
360 | refineDays : Integer -> Either PersianDateError (CalendarDate Persian)
361 | refineDays days = case choose
362 | (IotaTime.Calendar.isValidDays {calendar = Persian} days) of
363 | Left valid => Right (fromDays days @{valid})
364 | Right _ => Left (InvalidPersianDayCount days)
367 | nthDayOfMonth : DayNth -> DayOfWeek -> PersianMonth -> Year ->
369 | nthDayOfMonth nth target valueMonth valueYear =
370 | let monthLength = maxDaysInMonth valueMonth valueYear
371 | firstOffset = (weekdayNumber target -
372 | weekdayNumber (weekdayFromDays
373 | (daysFromCivil valueYear valueMonth 1))) `mod` daysPerWeek
374 | lastOffset = (weekdayNumber (weekdayFromDays
375 | (daysFromCivil valueYear valueMonth monthLength)) -
376 | weekdayNumber target) `mod` daysPerWeek
377 | dayNumber = nthWeekdayDayNumber nth (dayOfMonthValue monthLength)
378 | firstOffset lastOffset
379 | in dayOfMonthFromInteger dayNumber
382 | isValidNthDay : DayNth -> DayOfWeek -> PersianMonth -> Year -> Bool
383 | isValidNthDay nth target valueMonth valueYear =
384 | yearValue valueYear >= minimumYear &&
385 | yearValue valueYear <= maximumYear && case nth of
386 | Fifth => nthDayOfMonth nth target valueMonth valueYear <=
387 | maxDaysInMonth valueMonth valueYear
392 | fromNthDay : (nth : DayNth) -> (target : DayOfWeek) ->
393 | (valueMonth : PersianMonth) -> (valueYear : Year) ->
395 | (isValidNthDay nth target valueMonth valueYear)} ->
396 | CalendarDate Persian
397 | fromNthDay nth target valueMonth valueYear =
398 | makePersianDate (daysFromCivil valueYear valueMonth
399 | (nthDayOfMonth nth target valueMonth valueYear))
403 | refineNthDay : DayNth -> DayOfWeek -> PersianMonth -> Year ->
404 | Either PersianDateError (CalendarDate Persian)
405 | refineNthDay nth target valueMonth valueYear =
406 | case choose (isValidNthDay nth target valueMonth valueYear) of
407 | Left valid => Right
408 | (fromNthDay nth target valueMonth valueYear @{valid})
409 | Right _ => Left (InvalidPersianNthDay nth target valueMonth valueYear)
412 | weekDateDays : WeekNumber -> DayOfWeek -> Year -> Integer
413 | weekDateDays week target valueYear =
414 | let firstDay = daysFromCivil valueYear PersianMonths.Farvardin 1
415 | firstWeekStart = firstDay -
416 | ((weekdayNumber (weekdayFromDays firstDay) - 6)
418 | targetOffset = (weekdayNumber target - 6) `mod` 7
419 | in firstWeekStart + 7 * (weekNumberValue week - 1) + targetOffset
422 | isValidWeekDate : WeekNumber -> DayOfWeek -> Year -> Bool
423 | isValidWeekDate week target valueYear =
424 | let days = weekDateDays week target valueYear
425 | in yearValue valueYear >= minimumYear &&
426 | yearValue valueYear <= maximumYear &&
427 | IotaTime.Calendar.isValidDays {calendar = Persian} days
431 | fromWeekDate : (week : WeekNumber) -> (target : DayOfWeek) ->
432 | (valueYear : Year) ->
433 | {auto 0 valid : So (isValidWeekDate week target valueYear)} ->
434 | CalendarDate Persian
435 | fromWeekDate week target valueYear =
436 | makePersianDate (weekDateDays week target valueYear)
440 | refineWeekDate : WeekNumber -> DayOfWeek -> Year ->
441 | Either PersianDateError (CalendarDate Persian)
442 | refineWeekDate week target valueYear =
443 | case choose (isValidWeekDate week target valueYear) of
444 | Left valid => Right (fromWeekDate week target valueYear @{valid})
445 | Right _ => Left (InvalidPersianWeekDate week target valueYear)
449 | data PersianArithmeticRule = Simple | Birashk
453 | data ArithmeticPersian : PersianArithmeticRule -> Type where
454 | MkArithmeticPersian : ArithmeticPersian rule
458 | PersianSimple : Type
459 | PersianSimple = ArithmeticPersian Simple
463 | PersianArithmetic : Type
464 | PersianArithmetic = ArithmeticPersian Birashk
466 | simpleLeapPositions : List Integer
467 | simpleLeapPositions = [1, 5, 9, 13, 17, 22, 26, 30]
469 | countAtMost : Integer -> List Integer -> Integer
470 | countAtMost _ [] = 0
471 | countAtMost limit (value :: rest) =
472 | if value <= limit then 1 + countAtMost limit rest else 0
474 | simpleLeapsBefore : Integer -> Integer
475 | simpleLeapsBefore year =
476 | let elapsed = year - 1
477 | in (elapsed `div` 33) * 8 +
478 | countAtMost (elapsed `mod` 33) simpleLeapPositions
480 | simpleNewYear : Integer -> Integer
481 | simpleNewYear year = -
503285 + (year - 1) * 365 + simpleLeapsBefore year
483 | arithmeticNewYear : Integer -> Integer
484 | arithmeticNewYear year =
485 | let base = year - 474
486 | cycleYear = 474 + base `mod` 2820
487 | in -
503284 + ((cycleYear * 682 - 110) `div` 2816) +
488 | (cycleYear - 1) * 365 + (base `div` 2820) * 1029983
490 | arithmeticRuleEpoch : PersianArithmeticRule -> Integer
491 | arithmeticRuleEpoch Simple = -
503285
492 | arithmeticRuleEpoch Birashk = -
503284
494 | arithmeticRuleLastDay : PersianArithmeticRule -> Integer
495 | arithmeticRuleLastDay Simple = simpleNewYear 9378 - 1
496 | arithmeticRuleLastDay Birashk = arithmeticNewYear 9378 - 1
499 | arithmeticRuleName : PersianArithmeticRule -> String
500 | arithmeticRuleName Simple = "Persian Simple"
501 | arithmeticRuleName Birashk = "Persian Arithmetic"
503 | arithmeticRuleConstructorName : PersianArithmeticRule -> String
504 | arithmeticRuleConstructorName Simple = "simpleCalendarDate"
505 | arithmeticRuleConstructorName Birashk = "arithmeticCalendarDate"
507 | arithmeticRuleIsLeapYear : PersianArithmeticRule -> Integer -> Bool
508 | arithmeticRuleIsLeapYear Simple year =
509 | simpleNewYear (year + 1) - simpleNewYear year == 366
510 | arithmeticRuleIsLeapYear Birashk year =
511 | let cycleYear = (year - 474) `mod` 2820 + 474
512 | in ((cycleYear + 38) * 31) `mod` 128 < 31
514 | arithmeticRuleNewYearDay : PersianArithmeticRule -> Integer -> Integer
515 | arithmeticRuleNewYearDay Simple = simpleNewYear
516 | arithmeticRuleNewYearDay Birashk = arithmeticNewYear
519 | record ArithmeticPersianDate (rule : PersianArithmeticRule) where
520 | constructor MkArithmeticPersianDate
521 | arithmeticDaysSinceEpoch : Integer
523 | (arithmeticDaysSinceEpoch >= arithmeticRuleEpoch rule &&
524 | arithmeticDaysSinceEpoch <= arithmeticRuleLastDay rule)
527 | Eq (ArithmeticPersianDate rule) where
528 | left == right = left.arithmeticDaysSinceEpoch == right.arithmeticDaysSinceEpoch
531 | Ord (ArithmeticPersianDate rule) where
532 | compare left right = compare left.arithmeticDaysSinceEpoch right.arithmeticDaysSinceEpoch
534 | checkedArithmeticPersianDate : {rule : PersianArithmeticRule} ->
535 | (days : Integer) ->
537 | (days >= arithmeticRuleEpoch rule &&
538 | days <= arithmeticRuleLastDay rule)) ->
539 | ArithmeticPersianDate rule
540 | checkedArithmeticPersianDate days valid = MkArithmeticPersianDate days valid
542 | fromArithmeticPersianDays : {rule : PersianArithmeticRule} ->
543 | (days : Integer) ->
545 | (days >= arithmeticRuleEpoch rule &&
546 | days <= arithmeticRuleLastDay rule)} ->
547 | ArithmeticPersianDate rule
548 | fromArithmeticPersianDays days @{valid} =
549 | checkedArithmeticPersianDate days valid
552 | minimumArithmeticYear : Integer
553 | minimumArithmeticYear = 1
556 | maximumArithmeticYear : Integer
557 | maximumArithmeticYear = 9377
560 | isArithmeticLeapYear : {rule : PersianArithmeticRule} ->
562 | isArithmeticLeapYear {rule} value =
563 | arithmeticRuleIsLeapYear rule (yearValue value)
566 | arithmeticNewYearDay : {rule : PersianArithmeticRule} ->
568 | arithmeticNewYearDay {rule} value =
569 | arithmeticRuleNewYearDay rule (yearValue value)
573 | arithmeticLastDay : {rule : PersianArithmeticRule} ->
575 | arithmeticLastDay {rule} =
576 | arithmeticRuleNewYearDay rule (maximumArithmeticYear + 1) - 1
579 | maxArithmeticDaysInMonth : {rule : PersianArithmeticRule} ->
580 | PersianMonth -> Year -> DayOfMonth
581 | maxArithmeticDaysInMonth {rule} PersianMonths.Esfand value =
582 | if isArithmeticLeapYear {rule} value then 30 else 29
583 | maxArithmeticDaysInMonth valueMonth _ =
584 | if PersianMonths.monthNumber valueMonth <= 6 then 31 else 30
587 | isValidArithmeticDate : {rule : PersianArithmeticRule} ->
588 | DayOfMonth -> PersianMonth -> Year -> Bool
589 | isValidArithmeticDate {rule} valueDay valueMonth valueYear =
590 | let dayNumber = dayOfMonthValue valueDay
591 | yearNumber = yearValue valueYear
592 | maxDay = dayOfMonthValue
593 | (maxArithmeticDaysInMonth {rule} valueMonth valueYear)
594 | in dayNumber >= 1 && dayNumber <= maxDay &&
595 | yearNumber >= minimumArithmeticYear &&
596 | yearNumber <= maximumArithmeticYear
599 | arithmeticDaysFromCivil : {rule : PersianArithmeticRule} ->
600 | Year -> PersianMonth -> DayOfMonth -> Integer
601 | arithmeticDaysFromCivil {rule} valueYear valueMonth valueDay =
602 | arithmeticNewYearDay {rule} valueYear + monthOffset valueMonth +
603 | dayOfMonthValue valueDay - 1
605 | findArithmeticPersianYear : {rule : PersianArithmeticRule} ->
606 | Nat -> Integer -> Integer -> Integer
607 | findArithmeticPersianYear {rule} Z estimate _ = estimate
608 | findArithmeticPersianYear {rule} (S fuel) estimate days =
609 | if days < arithmeticRuleNewYearDay rule estimate
610 | then findArithmeticPersianYear {rule} fuel (estimate - 1) days
611 | else if days >= arithmeticRuleNewYearDay rule (estimate + 1)
612 | then findArithmeticPersianYear {rule} fuel (estimate + 1) days
615 | arithmeticPersianCivilFromDays : {rule : PersianArithmeticRule} ->
616 | Integer -> (Year, PersianMonth, DayOfMonth)
617 | arithmeticPersianCivilFromDays {rule} value =
618 | let epoch = arithmeticRuleEpoch rule
619 | estimate = max minimumArithmeticYear
620 | (min maximumArithmeticYear ((value - epoch) `div` 365 + 1))
621 | yearNumber = findArithmeticPersianYear {rule} 9377 estimate value
622 | valueYear = yearFromInteger yearNumber
623 | dayOfYear = value - arithmeticNewYearDay {rule} valueYear
624 | monthNumber = if dayOfYear == 365 then 12
625 | else if dayOfYear < 186 then dayOfYear `div` 31 + 1
626 | else (dayOfYear - 186) `div` 30 + 7
627 | offset = if monthNumber <= 6 then (monthNumber - 1) * 31
628 | else 186 + (monthNumber - 7) * 30
629 | in (valueYear, monthFromNumber monthNumber,
630 | dayOfMonthFromInteger (dayOfYear - offset + 1))
633 | {rule : PersianArithmeticRule} ->
634 | HasCalendarBridge (ArithmeticPersianDate rule) where
635 | toBridgeDays = arithmeticDaysSinceEpoch
636 | acceptsBridgeDays value = value >= arithmeticRuleEpoch rule &&
637 | value <= arithmeticRuleLastDay rule
638 | fromBridgeDays = fromArithmeticPersianDays {rule}
639 | bridgeCalendarName = arithmeticRuleName rule
641 | clampToArithmeticPersian : {rule : PersianArithmeticRule} ->
643 | clampToArithmeticPersian {rule} =
644 | max (arithmeticRuleEpoch rule) . min (arithmeticRuleLastDay rule)
646 | makeArithmeticPersianDate : {rule : PersianArithmeticRule} ->
647 | Integer -> ArithmeticPersianDate rule
648 | makeArithmeticPersianDate {rule} days =
649 | let clamped = clampToArithmeticPersian {rule} days
651 | (clamped >= arithmeticRuleEpoch rule &&
652 | clamped <= arithmeticRuleLastDay rule) of
653 | Left valid => checkedArithmeticPersianDate clamped valid
654 | Right _ => case rule of
655 | Simple => checkedArithmeticPersianDate (-
503285) Oh
656 | Birashk => checkedArithmeticPersianDate (-
503284) Oh
658 | shiftArithmeticPersianDays : {rule : PersianArithmeticRule} ->
659 | Integer -> ArithmeticPersianDate rule ->
660 | ArithmeticPersianDate rule
661 | shiftArithmeticPersianDays {rule} amount date =
662 | makeArithmeticPersianDate {rule} (date.arithmeticDaysSinceEpoch + amount)
664 | shiftArithmeticPersianMonths : {rule : PersianArithmeticRule} ->
665 | Integer -> ArithmeticPersianDate rule ->
666 | ArithmeticPersianDate rule
667 | shiftArithmeticPersianMonths {rule} amount date =
668 | let (valueYear, valueMonth, valueDay) =
669 | arithmeticPersianCivilFromDays {rule} date.arithmeticDaysSinceEpoch
670 | monthOrdinal = PersianMonths.monthNumber valueMonth - 1 + amount
671 | targetYear = yearFromInteger (yearValue valueYear + monthOrdinal `div` 12)
672 | targetMonth = monthFromNumber (monthOrdinal `mod` 12 + 1)
673 | targetDay = min valueDay
674 | (maxArithmeticDaysInMonth {rule} targetMonth targetYear)
675 | in makeArithmeticPersianDate {rule}
676 | (arithmeticDaysFromCivil {rule} targetYear targetMonth targetDay)
678 | shiftArithmeticPersianYears : {rule : PersianArithmeticRule} ->
679 | Integer -> ArithmeticPersianDate rule ->
680 | ArithmeticPersianDate rule
681 | shiftArithmeticPersianYears {rule} amount date =
682 | let (valueYear, valueMonth, valueDay) =
683 | arithmeticPersianCivilFromDays {rule} date.arithmeticDaysSinceEpoch
684 | targetYear = yearFromInteger (yearValue valueYear + amount)
685 | targetDay = min valueDay
686 | (maxArithmeticDaysInMonth {rule} valueMonth targetYear)
687 | in makeArithmeticPersianDate {rule}
688 | (arithmeticDaysFromCivil {rule} targetYear valueMonth targetDay)
690 | applyArithmeticPersianPeriod : {rule : PersianArithmeticRule} ->
691 | Period target -> ArithmeticPersianDate rule ->
692 | ArithmeticPersianDate rule
693 | applyArithmeticPersianPeriod {rule} = applyDatePeriodWith
694 | (shiftArithmeticPersianYears {rule})
695 | (shiftArithmeticPersianMonths {rule})
696 | (shiftArithmeticPersianDays {rule})
698 | arithmeticPersianDayOfWeek : ArithmeticPersianDate rule -> DayOfWeek
699 | arithmeticPersianDayOfWeek date =
700 | weekdayFromDays date.arithmeticDaysSinceEpoch
702 | nextArithmeticPersian : {rule : PersianArithmeticRule} ->
703 | Integer -> DayOfWeek ->
704 | ArithmeticPersianDate rule -> ArithmeticPersianDate rule
705 | nextArithmeticPersian {rule} count target date =
706 | makeArithmeticPersianDate {rule} (date.arithmeticDaysSinceEpoch +
707 | nextWeekdayOffset count (arithmeticPersianDayOfWeek date) target)
709 | previousArithmeticPersian : {rule : PersianArithmeticRule} ->
710 | Integer -> DayOfWeek ->
711 | ArithmeticPersianDate rule -> ArithmeticPersianDate rule
712 | previousArithmeticPersian {rule} count target date =
713 | makeArithmeticPersianDate {rule} (date.arithmeticDaysSinceEpoch +
714 | previousWeekdayOffset count
715 | (arithmeticPersianDayOfWeek date) target)
718 | {rule : PersianArithmeticRule} ->
719 | Calendar (ArithmeticPersian rule) where
720 | DateRep = ArithmeticPersianDate rule
721 | MonthRep _ = PersianMonth
723 | isValidDays value = value >= arithmeticRuleEpoch rule &&
724 | value <= arithmeticRuleLastDay rule
725 | fromDays = fromArithmeticPersianDays {rule}
726 | toDaysFor date = date.arithmeticDaysSinceEpoch
727 | toDaysValid (MkArithmeticPersianDate _ valid) = valid
728 | toFromDays _ _ = Refl
729 | fromToDays (MkArithmeticPersianDate _ _) = Refl
730 | calendarName = arithmeticRuleName rule
732 | year' date = let (value, _, _) = arithmeticPersianCivilFromDays {rule}
733 | date.arithmeticDaysSinceEpoch in value
734 | toYmd date = let (_, valueMonth, valueDay) =
735 | arithmeticPersianCivilFromDays {rule}
736 | date.arithmeticDaysSinceEpoch
737 | in (valueMonth, valueDay)
738 | day' date = let (_, _, value) = arithmeticPersianCivilFromDays {rule}
739 | date.arithmeticDaysSinceEpoch in value
740 | month' date = let (_, value, _) = arithmeticPersianCivilFromDays {rule}
741 | date.arithmeticDaysSinceEpoch in value
743 | applyCalendarPeriod' = applyArithmeticPersianPeriod {rule}
744 | shiftCalendarDays' = shiftArithmeticPersianDays {rule}
745 | dayOfWeekFor = arithmeticPersianDayOfWeek
746 | nextFor = nextArithmeticPersian {rule}
747 | previousFor = previousArithmeticPersian {rule}
750 | {rule : PersianArithmeticRule} ->
751 | Show (ArithmeticPersianDate rule) where
752 | show date = case arithmeticPersianCivilFromDays {rule}
753 | date.arithmeticDaysSinceEpoch of
754 | (valueYear, valueMonth, valueDay) =>
755 | arithmeticRuleConstructorName rule ++ " " ++ show valueDay ++
756 | " " ++ show valueMonth ++ " " ++ show valueYear
759 | HasCalendar (ArithmeticPersianDate rule) where
760 | calendarCapability = ()
763 | {rule : PersianArithmeticRule} ->
764 | PeriodTarget (ArithmeticPersianDate rule) where
768 | {rule : PersianArithmeticRule} ->
769 | ApplyPeriod (ArithmeticPersianDate rule) where
770 | applyPeriod = applyArithmeticPersianPeriod {rule}
773 | {rule : PersianArithmeticRule} ->
774 | CalendarValue (ArithmeticPersianDate rule) where
775 | CalendarMonth _ = PersianMonth
776 | calendarValueToDays = toDaysFor {calendar = ArithmeticPersian rule}
777 | calendarValueYear = yearFor {calendar = ArithmeticPersian rule}
778 | calendarValueMonthDay = toYmd {calendar = ArithmeticPersian rule}
779 | calendarValueDayOfWeek = dayOfWeekFor {calendar = ArithmeticPersian rule}
780 | calendarValueBetweenWith =
781 | betweenWithFor {calendar = ArithmeticPersian rule}
784 | {rule : PersianArithmeticRule} ->
785 | CalendarNavigation (ArithmeticPersianDate rule) where
786 | calendarValueNext = nextFor {calendar = ArithmeticPersian rule}
787 | calendarValuePrevious = previousFor {calendar = ArithmeticPersian rule}
791 | arithmeticRuleCalendarDate : {rule : PersianArithmeticRule} ->
792 | (valueDay : DayOfMonth) -> (valueMonth : PersianMonth) ->
793 | (valueYear : Year) ->
795 | (isValidArithmeticDate {rule} valueDay valueMonth valueYear)} ->
796 | CalendarDate (ArithmeticPersian rule)
797 | arithmeticRuleCalendarDate {rule} valueDay valueMonth valueYear =
798 | makeArithmeticPersianDate {rule}
799 | (arithmeticDaysFromCivil {rule} valueYear valueMonth valueDay)
803 | simpleCalendarDate : (valueDay : DayOfMonth) -> (valueMonth : PersianMonth) ->
804 | (valueYear : Year) ->
806 | (isValidArithmeticDate {rule = Simple}
807 | valueDay valueMonth valueYear)} -> CalendarDate PersianSimple
808 | simpleCalendarDate = arithmeticRuleCalendarDate {rule = Simple}
812 | arithmeticCalendarDate : (valueDay : DayOfMonth) ->
813 | (valueMonth : PersianMonth) -> (valueYear : Year) ->
815 | (isValidArithmeticDate {rule = Birashk}
816 | valueDay valueMonth valueYear)} -> CalendarDate PersianArithmetic
817 | arithmeticCalendarDate = arithmeticRuleCalendarDate {rule = Birashk}
821 | refineArithmeticRuleDate : {rule : PersianArithmeticRule} ->
822 | DayOfMonth -> PersianMonth -> Year ->
823 | Either PersianDateError (CalendarDate (ArithmeticPersian rule))
824 | refineArithmeticRuleDate {rule} valueDay valueMonth valueYear =
825 | case choose (isValidArithmeticDate {rule}
826 | valueDay valueMonth valueYear) of
827 | Left valid => Right (arithmeticRuleCalendarDate {rule}
828 | valueDay valueMonth valueYear {valid = valid})
829 | Right _ => Left (InvalidPersianDate valueDay valueMonth valueYear)
832 | refineSimpleDate : DayOfMonth -> PersianMonth -> Year ->
833 | Either PersianDateError (CalendarDate PersianSimple)
834 | refineSimpleDate = refineArithmeticRuleDate {rule = Simple}
837 | refineArithmeticDate : DayOfMonth -> PersianMonth -> Year ->
838 | Either PersianDateError (CalendarDate PersianArithmetic)
839 | refineArithmeticDate = refineArithmeticRuleDate {rule = Birashk}
843 | refineArithmeticDays : {rule : PersianArithmeticRule} ->
845 | Either PersianDateError (CalendarDate (ArithmeticPersian rule))
846 | refineArithmeticDays {rule} days =
848 | (IotaTime.Calendar.isValidDays
849 | {calendar = ArithmeticPersian rule} days) of
850 | Left valid => Right (fromDays {calendar = ArithmeticPersian rule}
851 | days {valid = valid})
852 | Right _ => Left (InvalidPersianDayCount days)
854 | arithmeticNthDayOfMonth : {rule : PersianArithmeticRule} ->
855 | DayNth -> DayOfWeek ->
856 | PersianMonth -> Year -> DayOfMonth
857 | arithmeticNthDayOfMonth {rule} nth target valueMonth valueYear =
858 | let monthLength = maxArithmeticDaysInMonth {rule} valueMonth valueYear
859 | firstOffset = (weekdayNumber target -
860 | weekdayNumber (weekdayFromDays
861 | (arithmeticDaysFromCivil {rule} valueYear valueMonth 1)))
863 | lastOffset = (weekdayNumber (weekdayFromDays
864 | (arithmeticDaysFromCivil {rule}
865 | valueYear valueMonth monthLength)) -
866 | weekdayNumber target) `mod` daysPerWeek
867 | dayNumber = nthWeekdayDayNumber nth (dayOfMonthValue monthLength)
868 | firstOffset lastOffset
869 | in dayOfMonthFromInteger dayNumber
872 | isValidArithmeticNthDay : {rule : PersianArithmeticRule} ->
873 | DayNth -> DayOfWeek ->
874 | PersianMonth -> Year -> Bool
875 | isValidArithmeticNthDay {rule} nth target valueMonth valueYear =
876 | yearValue valueYear >= minimumArithmeticYear &&
877 | yearValue valueYear <= maximumArithmeticYear && case nth of
878 | Fifth => arithmeticNthDayOfMonth {rule}
879 | nth target valueMonth valueYear <=
880 | maxArithmeticDaysInMonth {rule} valueMonth valueYear
885 | arithmeticFromNthDay : {rule : PersianArithmeticRule} ->
886 | (nth : DayNth) -> (target : DayOfWeek) ->
887 | (valueMonth : PersianMonth) -> (valueYear : Year) ->
889 | (isValidArithmeticNthDay {rule}
890 | nth target valueMonth valueYear)} ->
891 | CalendarDate (ArithmeticPersian rule)
892 | arithmeticFromNthDay {rule} nth target valueMonth valueYear =
893 | makeArithmeticPersianDate {rule} (arithmeticDaysFromCivil {rule}
894 | valueYear valueMonth
895 | (arithmeticNthDayOfMonth {rule} nth target valueMonth valueYear))
899 | refineArithmeticNthDay : {rule : PersianArithmeticRule} ->
900 | DayNth -> DayOfWeek ->
901 | PersianMonth -> Year ->
902 | Either PersianDateError (CalendarDate (ArithmeticPersian rule))
903 | refineArithmeticNthDay {rule} nth target valueMonth valueYear =
904 | case choose (isValidArithmeticNthDay {rule}
905 | nth target valueMonth valueYear) of
906 | Left valid => Right (arithmeticFromNthDay {rule}
907 | nth target valueMonth valueYear {valid = valid})
908 | Right _ => Left (InvalidPersianNthDay nth target valueMonth valueYear)
910 | arithmeticWeekDateDays : {rule : PersianArithmeticRule} ->
911 | WeekNumber -> DayOfWeek ->
913 | arithmeticWeekDateDays {rule} week target valueYear =
914 | let firstDay = arithmeticDaysFromCivil {rule}
915 | valueYear PersianMonths.Farvardin 1
916 | firstWeekStart = firstDay -
917 | ((weekdayNumber (weekdayFromDays firstDay) - 6)
919 | targetOffset = (weekdayNumber target - 6) `mod` 7
920 | in firstWeekStart + 7 * (weekNumberValue week - 1) + targetOffset
923 | isValidArithmeticWeekDate : {rule : PersianArithmeticRule} ->
924 | WeekNumber -> DayOfWeek ->
926 | isValidArithmeticWeekDate {rule} week target valueYear =
927 | let days = arithmeticWeekDateDays {rule} week target valueYear
928 | in yearValue valueYear >= minimumArithmeticYear &&
929 | yearValue valueYear <= maximumArithmeticYear &&
930 | IotaTime.Calendar.isValidDays
931 | {calendar = ArithmeticPersian rule} days
935 | arithmeticFromWeekDate : {rule : PersianArithmeticRule} ->
936 | (week : WeekNumber) -> (target : DayOfWeek) -> (valueYear : Year) ->
938 | (isValidArithmeticWeekDate {rule} week target valueYear)} ->
939 | CalendarDate (ArithmeticPersian rule)
940 | arithmeticFromWeekDate {rule} week target valueYear =
941 | makeArithmeticPersianDate {rule}
942 | (arithmeticWeekDateDays {rule} week target valueYear)
946 | refineArithmeticWeekDate : {rule : PersianArithmeticRule} ->
947 | WeekNumber -> DayOfWeek -> Year ->
948 | Either PersianDateError (CalendarDate (ArithmeticPersian rule))
949 | refineArithmeticWeekDate {rule} week target valueYear =
950 | case choose (isValidArithmeticWeekDate {rule}
951 | week target valueYear) of
952 | Left valid => Right (arithmeticFromWeekDate {rule}
953 | week target valueYear {valid = valid})
954 | Right _ => Left (InvalidPersianWeekDate week target valueYear)