{-| Description: Calculation of bank holidays in Germany. This module computes general bank holidays. Most of these bank holidays are also public aka legal holidays throughout Germany. You can use 'isPublicHoliday' to check if a holiday is also a legal holiday. Note: There are even more public holidays in each federal state which are covered by the [@ExtraHolidays@]("Data.Time.Calendar.BankHoliday.Germany.ExtraHolidays") module of this package. You can test this package or just calculate a few bank holidays with GHCi: @ $ stack ghci --package time --package bank-holiday-germany ghci> import Data.Time ghci> import Data.Time.Calendar.BankHoliday.Germany ghci> isBankHoliday (fromGregorian 2024 5 1) -- Tag der Arbeit True ghci> isPublicHoliday ChristmasEve False ghci> holidaysBetween (fromGregorian 2024 12 1) (fromGregorian 2024 12 26) [(2024-12-24,ChristmasEve),(2024-12-25,ChristmasDay),(2024-12-26,SecondChristmasDay)] @ Resources: - https://de.wikipedia.org/wiki/Bankfeiertag -} module Data.Time.Calendar.BankHoliday.Germany ( BankHoliday(..), isBankHoliday, isPublicHoliday, calculateEasterSunday, holidaysBetween, fromDay, toDay, germanHolidayName, yearFromDay ) where import Prelude import Data.Time.Calendar import Data.Maybe -- | Data type specifying German bank holidays including Christmas Eve and New Year's Eve. -- -- Note: This type cannot be an instance of class 'Ord' because due to -- Easter day calculation the order can change from year to year. data BankHoliday = NewYearsDay -- ^ Neujahrstag | GoodFriday -- ^ Karfreitag | EasterMonday -- ^ Ostermontag | LabourDay -- ^ Tag der Arbeit | AscensionDay -- ^ Christi Himmelfahrt | WhitMonday -- ^ Pfingstmontag | GermanUnityDay -- ^ Tag der Deutschen Einheit | ChristmasEve -- ^ Heilig Abend | ChristmasDay -- ^ 1​. Weihnachtsfeiertag | SecondChristmasDay -- ^ 2​. Weihnachtsfeiertag | NewYearsEve -- ^ Silvestertag deriving (Enum, Eq, Bounded, Show, Read) -- | Check if a given day is a 'BankHoliday'. -- -- >>> isBankHoliday (fromGregorian 2024 1 1) -- True isBankHoliday :: Day -> Bool isBankHoliday = isJust . fromDay -- | Helper to extract the year from a date. -- -- >>> yearFromDay $ fromGregorian 2020 1 1 -- 2020 yearFromDay :: Day -> Year yearFromDay = (\(y, _, _) -> y) . toGregorian -- | Calculate Easter Sunday using Spencer's algorithm. calculateEasterSunday :: Year -> Day calculateEasterSunday year = let a = year `rem` 19 b = year `quot` 100 c = year `rem` 100 d = b `quot` 4 e = b `rem` 4 f = (b + 8) `quot` 25 g = (b - f + 1) `quot` 3 h = (19 * a + b - d - g + 15) `rem` 30 i = c `quot` 4 k = c `rem` 4 l = (32 + 2 * e + 2 * i - h - k) `rem` 7 m = (a + 11 * h + 22 * l) `quot` 451 n = (h + l - 7 * m + 114) `quot` 31 o = (h + l - 7 * m + 114) `rem` 31 in fromGregorian year (fromIntegral n) (fromIntegral o + 1) -- | Compute the date for a given year and bank holiday. -- -- >>> toDay 2024 LabourDay -- 2024-05-01 toDay :: Year -> BankHoliday -> Day toDay year NewYearsDay = fromGregorian year 1 1 toDay year GoodFriday = addDays (-2) (calculateEasterSunday year) toDay year EasterMonday = addDays 1 (calculateEasterSunday year) toDay year LabourDay = fromGregorian year 5 1 toDay year AscensionDay = addDays 39 (calculateEasterSunday year) toDay year WhitMonday = addDays 50 (calculateEasterSunday year) toDay year GermanUnityDay = fromGregorian year 10 3 toDay year ChristmasEve = fromGregorian year 12 24 toDay year ChristmasDay = fromGregorian year 12 25 toDay year SecondChristmasDay = fromGregorian year 12 26 toDay year NewYearsEve = fromGregorian year 12 31 -- | Compute 'Maybe' the holiday for a given date. -- -- Note: In some years, two bank holidays can fall on the same -- day. E.g. 'LabourDay' and 'AscensionDay' in 2008 are both on -- 2008-05-01. In such cases this function returns the bank holiday -- that is defined first in the 'BankHoliday' 'Enum'. -- -- >>> fromDay (fromGregorian 2024 1 1) -- Just NewYearsDay -- -- >>> fromDay (fromGregorian 2024 5 5) -- Nothing fromDay :: Day -> Maybe BankHoliday fromDay day = listToMaybe $ filter (\d -> day == toDay (yearFromDay day) d) [minBound..maxBound] -- | Compute pairs of date and holiday from start to end (inclusive). -- -- Note: In some years, two bank holidays can fall on the same -- day. In such cases only one of them is in the resulting list. -- See 'fromDay' for more information. -- -- >>> map snd $ holidaysBetween (fromGregorian 2024 12 25) (fromGregorian 2024 12 26) -- [ChristmasDay,SecondChristmasDay] holidaysBetween :: Day -> Day -> [(Day, BankHoliday)] holidaysBetween start end = catMaybes $ map (\d -> (d,) <$> fromDay d) [start..end] -- | Translate the holiday name to German. germanHolidayName :: BankHoliday -> String germanHolidayName d = case d of NewYearsDay -> "Neujahrstag" GoodFriday -> "Karfreitag" EasterMonday -> "Ostermontag" LabourDay -> "Tag der Arbeit" AscensionDay -> "Christi Himmelfahrt" WhitMonday -> "Pfingstmontag" GermanUnityDay -> "Tag der Deutschen Einheit" ChristmasEve -> "Heilig Abend" ChristmasDay -> "1. Weihnachtsfeiertag" SecondChristmasDay -> "2. Weihnachtsfeiertag" NewYearsEve -> "Silvestertag" -- | True only for German public holidays aka legal holidays. -- Chrismas Eve and New Year's Eve are bank holidays but not public holidays. isPublicHoliday :: BankHoliday -> Bool isPublicHoliday ChristmasEve = False isPublicHoliday NewYearsEve = False isPublicHoliday _ = True