173 lines
6.1 KiB
Haskell
173 lines
6.1 KiB
Haskell
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@cip.ifi.lmu.de>
|
|
--
|
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
{-|
|
|
Module: Utils.Holidays
|
|
Description: German bank holidays
|
|
|
|
Interfaces follows module Data.Time.Calendar.BankHoliday.EnglandAndWales
|
|
isBankHolidayArea uses laziness to provide some simple memoization for a fixed range of years
|
|
-}
|
|
module Utils.Holidays
|
|
( Feiertagsgebiet(..)
|
|
, feiertage
|
|
, bankHolidays, bankHolidaysArea, bankHolidaysAreaSet
|
|
, isBankHoliday, isBankHolidayArea
|
|
, isWeekend
|
|
, addBusinessDays
|
|
) where
|
|
|
|
import Import.NoModel
|
|
|
|
import qualified Data.Set as Set (Set, member)
|
|
import qualified Data.Map as Map
|
|
|
|
--import Data.Time.Calendar.WeekDate
|
|
import Data.Time.Calendar.Easter (gregorianEaster)
|
|
|
|
|
|
-- | Some areas / companies within Germany.
|
|
-- | The datatype is not yet complete.
|
|
data Feiertagsgebiet = Deutschland | Hessen | Bayern | Munich | Fraport
|
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
|
|
deriving anyclass (Universe, Finite)
|
|
|
|
-- | List the bank holidays for the given year >= 1995, in ascending order.
|
|
-- | Holidays on a weekend are legally considered holidays in some German states, hence
|
|
-- | the behaviour differs from Data.Time.Calendar.BankHoliday.EnglandAndWales by including holidays on Sundays.
|
|
-- | Included for compatibility with Data.Time.Calendar.BankHoliday.EnglandAndWales
|
|
bankHolidays :: Integer -> [Day]
|
|
bankHolidays = bankHolidaysArea Deutschland
|
|
|
|
-- | Bank holidays for a region within Germany and a given year >= 1995.
|
|
-- | Holidays may occur on a sunday.
|
|
-- | For convenience and compatibility.
|
|
bankHolidaysArea :: Feiertagsgebiet -> Integer -> [Day]
|
|
bankHolidaysArea land year = Map.keys $ feiertage land year
|
|
|
|
-- | Bank holidays for a region within Germany and a given year >= 1995.
|
|
-- | Holidays may occur on a sunday.
|
|
bankHolidaysAreaSet :: Feiertagsgebiet -> Integer -> Set.Set Day
|
|
bankHolidaysAreaSet land year = Map.keysSet $ feiertage land year
|
|
|
|
-- | Bank holidays for a region within Germany and a given year >= 1995,
|
|
-- | mapped to the german name of each day.
|
|
-- | Holidays may occur on a sunday.
|
|
feiertage :: Feiertagsgebiet -> Integer -> Map.Map Day String
|
|
feiertage land year = case land of
|
|
Deutschland -> standardHolidays
|
|
Bayern -> bavarianHolidays
|
|
Munich -> munichHolidays
|
|
Hessen -> hessianHolidays
|
|
Fraport -> fraportHolidays
|
|
where
|
|
easterSunday = gregorianEaster year
|
|
easterSundayPlus = flip addDays easterSunday
|
|
|
|
standardHolidays = Map.fromList
|
|
[ (fromGregorian year 1 1, "Neujahr")
|
|
, (easterSundayPlus (-2) , "Karfreitag")
|
|
, (easterSunday , "Ostersonntag")
|
|
, (easterSundayPlus 1 , "Ostermontag")
|
|
, (fromGregorian year 5 1, "Erster Mai")
|
|
, (easterSundayPlus 39 , "Himmelfahrt")
|
|
, (easterSundayPlus 49 , "Pfingstsonntag")
|
|
, (easterSundayPlus 50 , "Pfingstmontag")
|
|
, (fromGregorian year 10 3, "Tag der deutschen Einheit")
|
|
, (fromGregorian year 12 25, "Erster Weihnachtstag")
|
|
, (fromGregorian year 12 26, "Zweiter Weihnachtstag")
|
|
]
|
|
|
|
hessianHolidays = standardHolidays <> map_singleton
|
|
(easterSundayPlus 60 , "Fronleichnam")
|
|
|
|
bavarianHolidays = hessianHolidays <> Map.fromList
|
|
[ (fromGregorian year 1 6, "Heilige Drei Könige")
|
|
, (fromGregorian year 11 1, "Allerheiligen")
|
|
]
|
|
|
|
munichHolidays = bavarianHolidays <> map_singleton
|
|
(fromGregorian year 8 15, "Maria Himmelfahrt")
|
|
|
|
fraportHolidays = hessianHolidays <> Map.fromList
|
|
[ (fromGregorian year 12 24, "Heiligabend")
|
|
, (fromGregorian year 12 31, "Sylvester")
|
|
]
|
|
|
|
map_singleton = uncurry Map.singleton
|
|
|
|
-- | For compatibility with with Data.Time.Calendar.BankHoliday.EnglandAndWales
|
|
-- | only for works for year >= 1995
|
|
isBankHoliday :: Day -> Bool
|
|
isBankHoliday = isBankHolidayArea Deutschland
|
|
|
|
{-- Inefficient, since entire year of holidays is computed for each call
|
|
isBankHolidayArea :: Feiertagsgebiet -> Day -> Bool
|
|
isBankHolidayArea land dd = dd `Set.member` holidays
|
|
where
|
|
(year, _, _) = toGregorian dd
|
|
holidays = bankHolidaysAreaSet land year
|
|
-}
|
|
|
|
-- | Returns whether a day is a bank holiday for years >= 1995
|
|
-- | Repeated calls are handled efficiently using lazy memoization
|
|
isBankHolidayArea :: Feiertagsgebiet -> Day -> Bool
|
|
isBankHolidayArea land = ibha
|
|
where
|
|
landHoliday = Map.lookup land memoHolidays
|
|
ibha dd = dd `Set.member` holidays
|
|
where
|
|
(year, _, _) = toGregorian dd
|
|
holidays
|
|
| (Just hys) <- landHoliday
|
|
, (Just hds) <- index hys $ fromInteger $ year2index year = hds
|
|
| otherwise = bankHolidaysAreaSet land year
|
|
|
|
-- memoize holidays
|
|
memoHolidays :: Map.Map Feiertagsgebiet [Set.Set Day]
|
|
memoHolidays = Map.fromList [(land,
|
|
[bankHolidaysAreaSet land year
|
|
| i <- [0..], let year = index2year i])
|
|
| land <- universeF]
|
|
|
|
-- year with fastest access
|
|
memoTip :: Integer
|
|
memoTip = 2030
|
|
|
|
year2index :: Integer -> Integer
|
|
year2index y
|
|
| y < memoTip = 2 * (memoTip - y) - 1
|
|
| otherwise = 2 * (y - memoTip)
|
|
|
|
index2year :: Integer -> Integer
|
|
index2year y = result
|
|
where
|
|
(x,r) = y `divMod` 2
|
|
result | r == 0 = memoTip + x
|
|
| otherwise = memoTip - x - 1
|
|
|
|
-- | Test for Saturday/Sunday
|
|
isWeekend :: Day -> Bool
|
|
isWeekend = isWeekend' . dayOfWeek
|
|
where
|
|
isWeekend' :: WeekDay -> Bool
|
|
isWeekend' Sunday = True
|
|
isWeekend' Saturday = True
|
|
isWeekend' _ = False
|
|
|
|
-- | Always returns a business day.
|
|
-- | Saturday/Sunday/Holiday treated like next (n>=0) or previous (n<0) working day
|
|
addBusinessDays :: Feiertagsgebiet -> Integer -> Day -> Day
|
|
addBusinessDays land = abd
|
|
where
|
|
ibhal = isBankHolidayArea land
|
|
freeday dd = isWeekend dd || ibhal dd
|
|
abd n = abd' n
|
|
where
|
|
(fwd, bwd) | n >= 0 = (succ, pred)
|
|
| otherwise = (pred, succ)
|
|
abd' m dd | freeday dd = abd' m (fwd dd)
|
|
| m == 0 = dd
|
|
| otherwise = abd' (bwd m) (fwd dd)
|