hledger/hledger-lib/Hledger/Data/Period.hs
2016-07-29 11:41:42 -07:00

135 lines
5.6 KiB
Haskell

{-|
Manipulate the time periods typically used for reports with Period,
a richer abstraction that will probably replace DateSpan.
See also Types and Dates.
-}
module Hledger.Data.Period
where
import Data.Time.Calendar
import Data.Time.Calendar.WeekDate
import Data.Time.Format
import Text.Printf
import Hledger.Data.Types
-- | Convert Periods to DateSpans.
--
-- >>> periodAsDateSpan (MonthPeriod 2000 1) == DateSpan (Just $ fromGregorian 2000 1 1) (Just $ fromGregorian 2000 2 1)
-- True
periodAsDateSpan :: Period -> DateSpan
periodAsDateSpan (DayPeriod d) = DateSpan (Just d) (Just $ addDays 1 d)
periodAsDateSpan (WeekPeriod b) = DateSpan (Just b) (Just $ addDays 7 b)
periodAsDateSpan (MonthPeriod y m) = DateSpan (Just $ fromGregorian y m 1) (Just $ fromGregorian y' m' 1)
where
(y',m') | m==12 = (y+1,1)
| otherwise = (y,m+1)
periodAsDateSpan (QuarterPeriod y q) = DateSpan (Just $ fromGregorian y m 1) (Just $ fromGregorian y' m' 1)
where
(y', q') | q==4 = (y+1,1)
| otherwise = (y,q+1)
quarterAsMonth q = (q-1) * 3 + 1
m = quarterAsMonth q
m' = quarterAsMonth q'
periodAsDateSpan (YearPeriod y) = DateSpan (Just $ fromGregorian y 1 1) (Just $ fromGregorian (y+1) 1 1)
periodAsDateSpan (PeriodBetween b e) = DateSpan (Just b) (Just e)
periodAsDateSpan (PeriodFrom b) = DateSpan (Just b) Nothing
periodAsDateSpan (PeriodTo e) = DateSpan Nothing (Just e)
periodAsDateSpan (PeriodAll) = DateSpan Nothing Nothing
-- | Convert DateSpans to Periods.
--
-- >>> dateSpanAsPeriod $ DateSpan (Just $ fromGregorian 2000 1 1) (Just $ fromGregorian 2000 2 1)
-- MonthPeriod 2000 1
dateSpanAsPeriod :: DateSpan -> Period
dateSpanAsPeriod (DateSpan (Just b) (Just e)) = simplifyPeriod $ PeriodBetween b e
dateSpanAsPeriod (DateSpan (Just b) Nothing) = PeriodFrom b
dateSpanAsPeriod (DateSpan Nothing (Just e)) = PeriodTo e
dateSpanAsPeriod (DateSpan Nothing Nothing) = PeriodAll
-- | Convert PeriodBetweens to a more abstract period where possible.
--
-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 1 1 1) (fromGregorian 2 1 1)
-- YearPeriod 1
-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 10 1) (fromGregorian 2001 1 1)
-- QuarterPeriod 2000 4
-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 1) (fromGregorian 2000 3 1)
-- MonthPeriod 2000 2
-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2016 7 25) (fromGregorian 2016 8 1)
-- WeekPeriod 2016-07-25
-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 1 1) (fromGregorian 2000 1 2)
-- DayPeriod 2000-01-01
-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 28) (fromGregorian 2000 3 1)
-- PeriodBetween 2000-02-28 2000-03-01
-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 2 29) (fromGregorian 2000 3 1)
-- DayPeriod 2000-02-29
-- >>> simplifyPeriod $ PeriodBetween (fromGregorian 2000 12 31) (fromGregorian 2001 1 1)
-- DayPeriod 2000-12-31
--
simplifyPeriod :: Period -> Period
simplifyPeriod (PeriodBetween b e) =
case (toGregorian b, toGregorian e) of
-- a year
((by,1,1), (ey,1,1)) | by+1==ey -> YearPeriod by
-- a half-year
-- ((by,1,1), (ey,7,1)) | by==ey ->
-- ((by,7,1), (ey,1,1)) | by+1==ey ->
-- a quarter
((by,1,1), (ey,4,1)) | by==ey -> QuarterPeriod by 1
((by,4,1), (ey,7,1)) | by==ey -> QuarterPeriod by 2
((by,7,1), (ey,10,1)) | by==ey -> QuarterPeriod by 3
((by,10,1), (ey,1,1)) | by+1==ey -> QuarterPeriod by 4
-- a month
((by,bm,1), (ey,em,1)) | by==ey && bm+1==em -> MonthPeriod by bm
((by,12,1), (ey,1,1)) | by+1==ey -> MonthPeriod by 12
-- a week (two successive mondays),
-- YYYYwN ("week N of year YYYY")
-- _ | let ((by,bw,bd), (ey,ew,ed)) = (toWeekDate from, toWeekDate to) in by==ey && fw+1==tw && bd==1 && ed==1 ->
-- a week starting on a monday
_ | let ((by,bw,bd), (ey,ew,ed)) = (toWeekDate b, toWeekDate (addDays (-1) e))
in by==ey && bw==ew && bd==1 && ed==7 -> WeekPeriod b
-- a day
((by,bm,bd), (ey,em,ed)) |
(by==ey && bm==em && bd+1==ed) ||
(by+1==ey && bm==12 && em==1 && bd==31 && ed==1) || -- crossing a year boundary
(by==ey && bm+1==em && isLastDayOfMonth by bm bd && ed==1) -- crossing a month boundary
-> DayPeriod b
_ -> PeriodBetween b e
simplifyPeriod p = p
isLastDayOfMonth y m d =
case m of
1 -> d==31
2 | isLeapYear y -> d==29
| otherwise -> d==28
3 -> d==31
4 -> d==30
5 -> d==31
6 -> d==30
7 -> d==31
8 -> d==31
9 -> d==30
10 -> d==31
11 -> d==30
12 -> d==31
_ -> False
-- | Render a period as a compact display string suitable for user output.
--
-- >>> showPeriod (WeekPeriod (fromGregorian 2016 7 25))
-- "2016/07/25w30"
showPeriod (DayPeriod b) = formatTime defaultTimeLocale "%0C%y/%m/%dd" b -- DATEd
showPeriod (WeekPeriod b) = formatTime defaultTimeLocale "%0C%y/%m/%dw%V" b -- STARTDATEwYEARWEEK
showPeriod (MonthPeriod y m) = printf "%04d/%02d" y m -- YYYY/MM
showPeriod (QuarterPeriod y q) = printf "%04dq%d" y q -- YYYYqN
showPeriod (YearPeriod y) = printf "%04d" y -- YYYY
showPeriod (PeriodBetween b e) = formatTime defaultTimeLocale "%0C%y/%m/%d" b
++ formatTime defaultTimeLocale "-%0C%y/%m/%d" (addDays (-1) e) -- STARTDATE-INCLUSIVEENDDATE
showPeriod (PeriodFrom b) = formatTime defaultTimeLocale "%0C%y/%m/%d-" b -- STARTDATE-
showPeriod (PeriodTo e) = formatTime defaultTimeLocale "-%0C%y/%m/%d" (addDays (-1) e) -- -INCLUSIVEENDDATE
showPeriod PeriodAll = "-"