-- This file is part of hs-tax-ato
-- Copyright (C) 2018, 2023  Fraser Tweedale
--
-- hs-tax-ato is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

{-|

Types and functions related to financial years.

-}

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Data.Tax.ATO.FY
  ( Days
  , days
  , daysAll
  , daysNone
  , getDays
  , getFraction
  , FinancialYear
  , financialYear
  , financialYearRange
  , financialYearRangeFromProxy
  )
  where

import GHC.TypeLits
import Data.Proxy
import Data.Ratio ((%))

import Data.Time.Calendar (Day, Year, fromGregorian, isLeapYear, toGregorian)

type FinancialYear = KnownNat

daysInYear :: KnownNat n => Proxy n -> Integer
daysInYear :: forall (n :: Nat). KnownNat n => Proxy n -> Year
daysInYear Proxy n
proxy
  | Year -> Bool
isLeapYear (Proxy n -> Year
forall (n :: Nat) (proxy :: Nat -> *).
KnownNat n =>
proxy n -> Year
natVal Proxy n
proxy) = Year
366
  | Bool
otherwise                 = Year
365

-- | Some number of days in a year.  Use 'days' to construct.
newtype Days (n :: Nat) = Days
  { forall (n :: Nat). Days n -> Year
getDays :: Integer
  -- ^ Get the number of days, which is between 0 and 365/366 inclusive.
  }
  deriving (MonthOfYear -> Days n -> ShowS
[Days n] -> ShowS
Days n -> String
(MonthOfYear -> Days n -> ShowS)
-> (Days n -> String) -> ([Days n] -> ShowS) -> Show (Days n)
forall (n :: Nat). MonthOfYear -> Days n -> ShowS
forall (n :: Nat). [Days n] -> ShowS
forall (n :: Nat). Days n -> String
forall a.
(MonthOfYear -> a -> ShowS)
-> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: forall (n :: Nat). MonthOfYear -> Days n -> ShowS
showsPrec :: MonthOfYear -> Days n -> ShowS
$cshow :: forall (n :: Nat). Days n -> String
show :: Days n -> String
$cshowList :: forall (n :: Nat). [Days n] -> ShowS
showList :: [Days n] -> ShowS
Show)

-- | Construct a 'Days' value.  If out of range, the number of days
-- is clamped to 0 or 365/366 (no runtime errors).
days :: forall a. (FinancialYear a) => Integer -> Days a
days :: forall (a :: Nat). FinancialYear a => Year -> Days a
days = Year -> Days a
forall (n :: Nat). Year -> Days n
Days (Year -> Days a) -> (Year -> Year) -> Year -> Days a
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Year -> Year -> Year
forall a. Ord a => a -> a -> a
max Year
0 (Year -> Year) -> (Year -> Year) -> Year -> Year
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Year -> Year -> Year
forall a. Ord a => a -> a -> a
min (Proxy a -> Year
forall (n :: Nat). KnownNat n => Proxy n -> Year
daysInYear (Proxy a
forall {k} (t :: k). Proxy t
Proxy :: Proxy a))

-- | Every day of the year
daysAll :: forall a. (FinancialYear a) => Days a
daysAll :: forall (a :: Nat). FinancialYear a => Days a
daysAll = Year -> Days a
forall (n :: Nat). Year -> Days n
Days (Proxy a -> Year
forall (n :: Nat). KnownNat n => Proxy n -> Year
daysInYear (Proxy a
forall {k} (t :: k). Proxy t
Proxy :: Proxy a))

-- | Zero days of the year
daysNone :: Days a
daysNone :: forall (a :: Nat). Days a
daysNone = Year -> Days a
forall (n :: Nat). Year -> Days n
Days Year
0

-- | Get the number of days as a fractional value.
-- The denominator is determined by the year type.
--
getFraction :: forall a frac. (FinancialYear a, Fractional frac) => Days a -> frac
getFraction :: forall (a :: Nat) frac.
(FinancialYear a, Fractional frac) =>
Days a -> frac
getFraction Days a
n = Rational -> frac
forall a. Fractional a => Rational -> a
fromRational (Rational -> frac) -> Rational -> frac
forall a b. (a -> b) -> a -> b
$ Days a -> Year
forall (n :: Nat). Days n -> Year
getDays Days a
n Year -> Year -> Rational
forall a. Integral a => a -> a -> Ratio a
% Proxy a -> Year
forall (n :: Nat). KnownNat n => Proxy n -> Year
daysInYear (Proxy a
forall {k} (t :: k). Proxy t
Proxy :: Proxy a)

-- | The financial year in which the given day falls.
--
financialYear :: Day -> Year
financialYear :: Day -> Year
financialYear Day
d = case Day -> (Year, MonthOfYear, MonthOfYear)
toGregorian Day
d of
  (Year
y, MonthOfYear
m, MonthOfYear
_)
    | MonthOfYear
m MonthOfYear -> MonthOfYear -> Bool
forall a. Ord a => a -> a -> Bool
>= MonthOfYear
7{-July-}  -> Year
y Year -> Year -> Year
forall a. Num a => a -> a -> a
+ Year
1
    | Bool
otherwise       -> Year
y

-- | Get the range of days (inclusive) for the financial year (July to June)
-- ending in the given year.
financialYearRange :: Year -> (Day, Day)
financialYearRange :: Year -> (Day, Day)
financialYearRange Year
y =
  ( Year -> MonthOfYear -> MonthOfYear -> Day
fromGregorian (Year
y Year -> Year -> Year
forall a. Num a => a -> a -> a
- Year
1) MonthOfYear
7{-July-} MonthOfYear
1
  , Year -> MonthOfYear -> MonthOfYear -> Day
fromGregorian Year
y       MonthOfYear
6{-June-} MonthOfYear
30
  )

-- | Get the financial year range (inclusive) for the given type-level
-- financial year.  See also 'financialYearRange'.
financialYearRangeFromProxy :: forall n. (FinancialYear n) => Proxy n -> (Day, Day)
financialYearRangeFromProxy :: forall (n :: Nat). FinancialYear n => Proxy n -> (Day, Day)
financialYearRangeFromProxy Proxy n
proxy = Year -> (Day, Day)
financialYearRange (Proxy n -> Year
forall (n :: Nat) (proxy :: Nat -> *).
KnownNat n =>
proxy n -> Year
natVal Proxy n
proxy)