2015-03-28 01:42:32 +03:00
|
|
|
{-# LANGUAGE CPP #-}
|
2012-11-14 21:25:02 +04:00
|
|
|
{-# LANGUAGE NoMonoLocalBinds #-}
|
2014-11-03 08:52:12 +03:00
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
2016-07-29 18:57:10 +03:00
|
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2017-07-27 14:59:55 +03:00
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2018-06-04 22:30:43 +03:00
|
|
|
{-# LANGUAGE PackageImports #-}
|
2008-11-11 15:34:05 +03:00
|
|
|
{-|
|
|
|
|
|
2009-12-13 01:19:57 +03:00
|
|
|
Date parsing and utilities for hledger.
|
|
|
|
|
2008-11-27 07:31:01 +03:00
|
|
|
For date and time values, we use the standard Day and UTCTime types.
|
|
|
|
|
2008-11-27 03:35:00 +03:00
|
|
|
A 'SmartDate' is a date which may be partially-specified or relative.
|
2009-12-13 01:19:57 +03:00
|
|
|
Eg 2008\/12\/31, but also 2008\/12, 12\/31, tomorrow, last week, next year.
|
2008-11-27 10:50:02 +03:00
|
|
|
We represent these as a triple of strings like (\"2008\",\"12\",\"\"),
|
|
|
|
(\"\",\"\",\"tomorrow\"), (\"\",\"last\",\"week\").
|
2008-11-11 15:34:05 +03:00
|
|
|
|
2008-11-27 07:31:01 +03:00
|
|
|
A 'DateSpan' is the span of time between two specific calendar dates, or
|
2008-12-04 02:20:38 +03:00
|
|
|
an open-ended span where one or both dates are unspecified. (A date span
|
|
|
|
with both ends unspecified matches all dates.)
|
|
|
|
|
2010-08-03 21:05:02 +04:00
|
|
|
An 'Interval' is ledger's \"reporting interval\" - weekly, monthly,
|
2008-12-04 02:20:38 +03:00
|
|
|
quarterly, etc.
|
2008-11-27 07:31:01 +03:00
|
|
|
|
2016-07-29 21:00:29 +03:00
|
|
|
'Period' will probably replace DateSpan in due course.
|
|
|
|
|
2008-11-11 15:34:05 +03:00
|
|
|
-}
|
|
|
|
|
2011-04-22 17:55:42 +04:00
|
|
|
-- XXX fromGregorian silently clips bad dates, use fromGregorianValid instead ?
|
|
|
|
|
2012-04-16 20:44:41 +04:00
|
|
|
module Hledger.Data.Dates (
|
|
|
|
-- * Misc date handling utilities
|
|
|
|
getCurrentDay,
|
|
|
|
getCurrentMonth,
|
|
|
|
getCurrentYear,
|
|
|
|
nulldate,
|
|
|
|
spanContainsDate,
|
2016-12-03 02:36:23 +03:00
|
|
|
periodContainsDate,
|
2012-12-06 04:28:23 +04:00
|
|
|
parsedateM,
|
2012-04-16 20:44:41 +04:00
|
|
|
parsedate,
|
|
|
|
showDate,
|
2013-09-27 02:06:48 +04:00
|
|
|
showDateSpan,
|
2018-01-18 22:46:12 +03:00
|
|
|
showDateSpanMonthAbbrev,
|
2012-04-16 20:44:41 +04:00
|
|
|
elapsedSeconds,
|
|
|
|
prevday,
|
2018-06-12 22:29:22 +03:00
|
|
|
periodexprp,
|
2012-04-16 20:44:41 +04:00
|
|
|
parsePeriodExpr,
|
2018-06-09 05:35:27 +03:00
|
|
|
parsePeriodExpr',
|
2012-04-16 20:44:41 +04:00
|
|
|
nulldatespan,
|
2018-10-17 23:10:49 +03:00
|
|
|
emptydatespan,
|
2012-04-16 20:44:41 +04:00
|
|
|
failIfInvalidYear,
|
2014-10-30 17:58:15 +03:00
|
|
|
failIfInvalidMonth,
|
|
|
|
failIfInvalidDay,
|
2012-04-16 20:44:41 +04:00
|
|
|
datesepchar,
|
|
|
|
datesepchars,
|
2018-05-22 04:52:34 +03:00
|
|
|
isDateSepChar,
|
2013-12-07 02:06:12 +04:00
|
|
|
spanStart,
|
|
|
|
spanEnd,
|
|
|
|
spansSpan,
|
2012-04-16 20:44:41 +04:00
|
|
|
spanIntersect,
|
2012-05-19 06:56:26 +04:00
|
|
|
spansIntersect,
|
2014-04-19 19:38:03 +04:00
|
|
|
spanDefaultsFrom,
|
2012-05-19 06:56:26 +04:00
|
|
|
spanUnion,
|
|
|
|
spansUnion,
|
2012-04-16 20:44:41 +04:00
|
|
|
smartdate,
|
|
|
|
splitSpan,
|
|
|
|
fixSmartDate,
|
|
|
|
fixSmartDateStr,
|
2012-04-16 21:09:27 +04:00
|
|
|
fixSmartDateStrEither,
|
2012-04-16 20:44:41 +04:00
|
|
|
fixSmartDateStrEither',
|
|
|
|
daysInSpan,
|
|
|
|
maybePeriod,
|
2012-05-19 06:56:26 +04:00
|
|
|
mkdatespan,
|
2012-04-16 20:44:41 +04:00
|
|
|
)
|
2008-11-22 15:18:19 +03:00
|
|
|
where
|
2008-11-11 15:34:05 +03:00
|
|
|
|
2019-12-01 20:31:36 +03:00
|
|
|
import Prelude ()
|
|
|
|
import "base-compat-batteries" Prelude.Compat hiding (fail)
|
lib, cli, ui: start using Control.Monad.Fail, allow base-compat 0.11
fail is moving out of Monad and into it's own MonadFail class.
This will be enforced in GHC 8.8 (I think).
base-compat/base-compat-batteries 0.11.0 have adapted to this,
and are approaching stackage nightly
(https://github.com/commercialhaskell/stackage/issues/4802).
hledger is now ready to build with base-compat-batteries 0.11.0, once
all of our deps do (eg aeson). We are still compatible with the older
0.10.x and GHC 7.10.3 as well.
For now we are using both fails:
- new fail (from Control.Monad.Fail), used in our parsers, imported
via base-compat-batteries Control.Monad.Fail.Compat to work with
older GHC versions.
- old fail (from GHC.Base, exported by Prelude, Control.Monad,
Control.Monad.State.Strict, Prelude.Compat, ...), used in easytest's
Test, since I couldn't find their existing fail implementation to update.
To reduce (my) confusion, these are imported carefully, consistently,
and qualified everywhere as Fail.fail and Prelude.fail, with clashing
re-exports suppressed, like so:
import Prelude hiding (fail)
import qualified Prelude (fail)
import Control.Monad.State.Strict hiding (fail)
import "base-compat-batteries" Prelude.Compat hiding (fail)
import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail
2019-09-09 03:13:47 +03:00
|
|
|
import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail (MonadFail, fail)
|
2018-09-30 04:32:08 +03:00
|
|
|
import Control.Applicative.Permutations
|
lib, cli, ui: start using Control.Monad.Fail, allow base-compat 0.11
fail is moving out of Monad and into it's own MonadFail class.
This will be enforced in GHC 8.8 (I think).
base-compat/base-compat-batteries 0.11.0 have adapted to this,
and are approaching stackage nightly
(https://github.com/commercialhaskell/stackage/issues/4802).
hledger is now ready to build with base-compat-batteries 0.11.0, once
all of our deps do (eg aeson). We are still compatible with the older
0.10.x and GHC 7.10.3 as well.
For now we are using both fails:
- new fail (from Control.Monad.Fail), used in our parsers, imported
via base-compat-batteries Control.Monad.Fail.Compat to work with
older GHC versions.
- old fail (from GHC.Base, exported by Prelude, Control.Monad,
Control.Monad.State.Strict, Prelude.Compat, ...), used in easytest's
Test, since I couldn't find their existing fail implementation to update.
To reduce (my) confusion, these are imported carefully, consistently,
and qualified everywhere as Fail.fail and Prelude.fail, with clashing
re-exports suppressed, like so:
import Prelude hiding (fail)
import qualified Prelude (fail)
import Control.Monad.State.Strict hiding (fail)
import "base-compat-batteries" Prelude.Compat hiding (fail)
import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail
2019-09-09 03:13:47 +03:00
|
|
|
import Control.Monad (unless)
|
2018-06-05 02:28:28 +03:00
|
|
|
import "base-compat-batteries" Data.List.Compat
|
2017-11-26 06:58:53 +03:00
|
|
|
import Data.Default
|
2011-05-28 08:11:44 +04:00
|
|
|
import Data.Maybe
|
2016-07-29 18:57:10 +03:00
|
|
|
import Data.Text (Text)
|
|
|
|
import qualified Data.Text as T
|
2015-03-28 01:42:32 +03:00
|
|
|
#if MIN_VERSION_time(1,5,0)
|
|
|
|
import Data.Time.Format hiding (months)
|
|
|
|
#else
|
2008-11-11 15:34:05 +03:00
|
|
|
import Data.Time.Format
|
2015-03-30 02:30:25 +03:00
|
|
|
import System.Locale (TimeLocale, defaultTimeLocale)
|
2015-03-28 01:42:32 +03:00
|
|
|
#endif
|
2011-05-28 08:11:44 +04:00
|
|
|
import Data.Time.Calendar
|
2008-11-27 04:49:13 +03:00
|
|
|
import Data.Time.Calendar.OrdinalDate
|
2011-05-28 08:11:44 +04:00
|
|
|
import Data.Time.Clock
|
|
|
|
import Data.Time.LocalTime
|
2013-12-07 02:06:12 +04:00
|
|
|
import Safe (headMay, lastMay, readMay)
|
2018-05-22 01:47:56 +03:00
|
|
|
import Text.Megaparsec
|
|
|
|
import Text.Megaparsec.Char
|
2018-09-30 04:32:08 +03:00
|
|
|
import Text.Megaparsec.Custom
|
2011-05-28 08:11:44 +04:00
|
|
|
import Text.Printf
|
|
|
|
|
2010-05-20 03:08:53 +04:00
|
|
|
import Hledger.Data.Types
|
2016-07-29 21:00:29 +03:00
|
|
|
import Hledger.Data.Period
|
2011-05-28 08:11:44 +04:00
|
|
|
import Hledger.Utils
|
2008-11-11 15:34:05 +03:00
|
|
|
|
2014-04-04 02:19:45 +04:00
|
|
|
|
|
|
|
-- Help ppShow parse and line-wrap DateSpans better in debug output.
|
|
|
|
instance Show DateSpan where
|
2016-05-07 03:23:24 +03:00
|
|
|
show s = "DateSpan " ++ showDateSpan s
|
|
|
|
-- show s = "DateSpan \"" ++ showDateSpan s ++ "\"" -- quotes to help pretty-show
|
2014-04-04 02:19:45 +04:00
|
|
|
|
2008-11-27 07:01:07 +03:00
|
|
|
showDate :: Day -> String
|
2014-03-22 14:31:13 +04:00
|
|
|
showDate = formatTime defaultTimeLocale "%0C%y/%m/%d"
|
2008-11-11 15:34:05 +03:00
|
|
|
|
2014-07-27 03:54:18 +04:00
|
|
|
-- | Render a datespan as a display string, abbreviating into a
|
|
|
|
-- compact form if possible.
|
2016-07-29 21:00:29 +03:00
|
|
|
showDateSpan :: DateSpan -> String
|
|
|
|
showDateSpan = showPeriod . dateSpanAsPeriod
|
2013-09-27 02:06:48 +04:00
|
|
|
|
2018-01-18 22:46:12 +03:00
|
|
|
-- | Like showDateSpan, but show month spans as just the abbreviated month name
|
|
|
|
-- in the current locale.
|
|
|
|
showDateSpanMonthAbbrev :: DateSpan -> String
|
|
|
|
showDateSpanMonthAbbrev = showPeriodMonthAbbrev . dateSpanAsPeriod
|
|
|
|
|
2011-03-11 21:45:57 +03:00
|
|
|
-- | Get the current local date.
|
2009-01-24 22:48:37 +03:00
|
|
|
getCurrentDay :: IO Day
|
2019-02-14 16:14:52 +03:00
|
|
|
getCurrentDay = localDay . zonedTimeToLocalTime <$> getZonedTime
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2011-08-07 19:31:36 +04:00
|
|
|
-- | Get the current local month number.
|
|
|
|
getCurrentMonth :: IO Int
|
2019-02-14 16:14:52 +03:00
|
|
|
getCurrentMonth = second3 . toGregorian <$> getCurrentDay
|
2011-08-07 19:31:36 +04:00
|
|
|
|
2011-03-11 21:45:57 +03:00
|
|
|
-- | Get the current local year.
|
|
|
|
getCurrentYear :: IO Integer
|
2019-02-14 16:14:52 +03:00
|
|
|
getCurrentYear = first3 . toGregorian <$> getCurrentDay
|
2011-03-11 21:45:57 +03:00
|
|
|
|
2008-11-27 07:01:07 +03:00
|
|
|
elapsedSeconds :: Fractional a => UTCTime -> UTCTime -> a
|
2009-09-22 19:56:59 +04:00
|
|
|
elapsedSeconds t1 = realToFrac . diffUTCTime t1
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2013-12-07 02:06:12 +04:00
|
|
|
spanStart :: DateSpan -> Maybe Day
|
|
|
|
spanStart (DateSpan d _) = d
|
|
|
|
|
|
|
|
spanEnd :: DateSpan -> Maybe Day
|
|
|
|
spanEnd (DateSpan _ d) = d
|
|
|
|
|
2014-09-11 00:07:53 +04:00
|
|
|
-- might be useful later: http://en.wikipedia.org/wiki/Allen%27s_interval_algebra
|
2014-04-19 22:45:47 +04:00
|
|
|
|
2013-12-07 02:06:12 +04:00
|
|
|
-- | Get overall span enclosing multiple sequentially ordered spans.
|
|
|
|
spansSpan :: [DateSpan] -> DateSpan
|
|
|
|
spansSpan spans = DateSpan (maybe Nothing spanStart $ headMay spans) (maybe Nothing spanEnd $ lastMay spans)
|
|
|
|
|
2018-03-29 20:09:15 +03:00
|
|
|
-- | Split a DateSpan into consecutive whole spans of the specified interval
|
|
|
|
-- which fully encompass the original span (and a little more when necessary).
|
2014-04-14 19:31:34 +04:00
|
|
|
-- If no interval is specified, the original span is returned.
|
2018-03-29 20:09:15 +03:00
|
|
|
-- If the original span is the null date span, ie unbounded, the null date span is returned.
|
|
|
|
-- If the original span is empty, eg if the end date is <= the start date, no spans are returned.
|
2019-07-15 13:28:52 +03:00
|
|
|
--
|
2016-05-07 04:05:42 +03:00
|
|
|
--
|
|
|
|
-- ==== Examples:
|
|
|
|
-- >>> let t i d1 d2 = splitSpan i $ mkdatespan d1 d2
|
|
|
|
-- >>> t NoInterval "2008/01/01" "2009/01/01"
|
|
|
|
-- [DateSpan 2008]
|
|
|
|
-- >>> t (Quarters 1) "2008/01/01" "2009/01/01"
|
|
|
|
-- [DateSpan 2008q1,DateSpan 2008q2,DateSpan 2008q3,DateSpan 2008q4]
|
|
|
|
-- >>> splitSpan (Quarters 1) nulldatespan
|
|
|
|
-- [DateSpan -]
|
|
|
|
-- >>> t (Days 1) "2008/01/01" "2008/01/01" -- an empty datespan
|
2018-03-29 20:09:15 +03:00
|
|
|
-- []
|
2016-05-07 04:05:42 +03:00
|
|
|
-- >>> t (Quarters 1) "2008/01/01" "2008/01/01"
|
2018-03-29 20:09:15 +03:00
|
|
|
-- []
|
2016-05-07 04:05:42 +03:00
|
|
|
-- >>> t (Months 1) "2008/01/01" "2008/04/01"
|
|
|
|
-- [DateSpan 2008/01,DateSpan 2008/02,DateSpan 2008/03]
|
|
|
|
-- >>> t (Months 2) "2008/01/01" "2008/04/01"
|
|
|
|
-- [DateSpan 2008/01/01-2008/02/29,DateSpan 2008/03/01-2008/04/30]
|
|
|
|
-- >>> t (Weeks 1) "2008/01/01" "2008/01/15"
|
|
|
|
-- [DateSpan 2007/12/31w01,DateSpan 2008/01/07w02,DateSpan 2008/01/14w03]
|
|
|
|
-- >>> t (Weeks 2) "2008/01/01" "2008/01/15"
|
|
|
|
-- [DateSpan 2007/12/31-2008/01/13,DateSpan 2008/01/14-2008/01/27]
|
|
|
|
-- >>> t (DayOfMonth 2) "2008/01/01" "2008/04/01"
|
2017-11-25 00:51:51 +03:00
|
|
|
-- [DateSpan 2007/12/02-2008/01/01,DateSpan 2008/01/02-2008/02/01,DateSpan 2008/02/02-2008/03/01,DateSpan 2008/03/02-2008/04/01]
|
2017-11-25 02:52:34 +03:00
|
|
|
-- >>> t (WeekdayOfMonth 2 4) "2011/01/01" "2011/02/15"
|
|
|
|
-- [DateSpan 2010/12/09-2011/01/12,DateSpan 2011/01/13-2011/02/09,DateSpan 2011/02/10-2011/03/09]
|
2016-05-07 04:05:42 +03:00
|
|
|
-- >>> t (DayOfWeek 2) "2011/01/01" "2011/01/15"
|
2017-11-25 00:51:51 +03:00
|
|
|
-- [DateSpan 2010/12/28-2011/01/03,DateSpan 2011/01/04-2011/01/10,DateSpan 2011/01/11-2011/01/17]
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> t (DayOfYear 11 29) "2011/10/01" "2011/10/15"
|
|
|
|
-- [DateSpan 2010/11/29-2011/11/28]
|
|
|
|
-- >>> t (DayOfYear 11 29) "2011/12/01" "2012/12/15"
|
|
|
|
-- [DateSpan 2011/11/29-2012/11/28,DateSpan 2012/11/29-2013/11/28]
|
2016-05-07 04:05:42 +03:00
|
|
|
--
|
2008-12-04 02:20:38 +03:00
|
|
|
splitSpan :: Interval -> DateSpan -> [DateSpan]
|
2009-06-05 13:44:20 +04:00
|
|
|
splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]
|
2018-03-29 20:09:15 +03:00
|
|
|
splitSpan _ s | isEmptySpan s = []
|
2011-01-14 07:32:08 +03:00
|
|
|
splitSpan NoInterval s = [s]
|
2011-01-14 08:01:00 +03:00
|
|
|
splitSpan (Days n) s = splitspan startofday (applyN n nextday) s
|
|
|
|
splitSpan (Weeks n) s = splitspan startofweek (applyN n nextweek) s
|
|
|
|
splitSpan (Months n) s = splitspan startofmonth (applyN n nextmonth) s
|
|
|
|
splitSpan (Quarters n) s = splitspan startofquarter (applyN n nextquarter) s
|
|
|
|
splitSpan (Years n) s = splitspan startofyear (applyN n nextyear) s
|
2017-11-25 01:44:10 +03:00
|
|
|
splitSpan (DayOfMonth n) s = splitspan (nthdayofmonthcontaining n) (nthdayofmonth n . nextmonth) s
|
2017-11-25 02:52:34 +03:00
|
|
|
splitSpan (WeekdayOfMonth n wd) s = splitspan (nthweekdayofmonthcontaining n wd) (advancetonthweekday n wd . nextmonth) s
|
2011-01-14 08:01:00 +03:00
|
|
|
splitSpan (DayOfWeek n) s = splitspan (nthdayofweekcontaining n) (applyN (n-1) nextday . nextweek) s
|
2018-07-30 13:05:02 +03:00
|
|
|
splitSpan (DayOfYear m n) s = splitspan (nthdayofyearcontaining m n) (applyN (n-1) nextday . applyN (m-1) nextmonth . nextyear) s
|
2011-01-14 07:32:08 +03:00
|
|
|
-- splitSpan (WeekOfYear n) s = splitspan startofweek (applyN n nextweek) s
|
|
|
|
-- splitSpan (MonthOfYear n) s = splitspan startofmonth (applyN n nextmonth) s
|
|
|
|
-- splitSpan (QuarterOfYear n) s = splitspan startofquarter (applyN n nextquarter) s
|
|
|
|
|
|
|
|
-- Split the given span using the provided helper functions:
|
|
|
|
-- start is applied to the span's start date to get the first sub-span's start date
|
|
|
|
-- next is applied to a sub-span's start date to get the next sub-span's start date
|
2011-01-14 08:01:00 +03:00
|
|
|
splitspan :: (Day -> Day) -> (Day -> Day) -> DateSpan -> [DateSpan]
|
|
|
|
splitspan _ _ (DateSpan Nothing Nothing) = []
|
|
|
|
splitspan start next (DateSpan Nothing (Just e)) = splitspan start next (DateSpan (Just $ start e) (Just $ next $ start e))
|
|
|
|
splitspan start next (DateSpan (Just s) Nothing) = splitspan start next (DateSpan (Just $ start s) (Just $ next $ start s))
|
|
|
|
splitspan start next span@(DateSpan (Just s) (Just e))
|
2011-01-14 07:32:08 +03:00
|
|
|
| s == e = [span]
|
2011-01-14 08:01:00 +03:00
|
|
|
| otherwise = splitspan' start next span
|
2009-06-05 13:44:20 +04:00
|
|
|
where
|
2011-01-14 08:01:00 +03:00
|
|
|
splitspan' start next (DateSpan (Just s) (Just e))
|
2011-01-14 07:32:08 +03:00
|
|
|
| s >= e = []
|
2011-01-14 08:01:00 +03:00
|
|
|
| otherwise = DateSpan (Just subs) (Just sube) : splitspan' start next (DateSpan (Just sube) (Just e))
|
2011-01-14 07:32:08 +03:00
|
|
|
where subs = start s
|
2011-01-14 08:01:00 +03:00
|
|
|
sube = next subs
|
|
|
|
splitspan' _ _ _ = error' "won't happen, avoids warnings"
|
2009-05-29 14:02:14 +04:00
|
|
|
|
|
|
|
-- | Count the days in a DateSpan, or if it is open-ended return Nothing.
|
|
|
|
daysInSpan :: DateSpan -> Maybe Integer
|
|
|
|
daysInSpan (DateSpan (Just d1) (Just d2)) = Just $ diffDays d2 d1
|
|
|
|
daysInSpan _ = Nothing
|
2010-07-11 22:56:36 +04:00
|
|
|
|
2018-03-29 20:09:15 +03:00
|
|
|
-- | Is this an empty span, ie closed with the end date on or before the start date ?
|
|
|
|
isEmptySpan :: DateSpan -> Bool
|
|
|
|
isEmptySpan s = case daysInSpan s of
|
|
|
|
Just n -> n < 1
|
|
|
|
Nothing -> False
|
|
|
|
|
2010-07-11 22:56:36 +04:00
|
|
|
-- | Does the span include the given date ?
|
|
|
|
spanContainsDate :: DateSpan -> Day -> Bool
|
|
|
|
spanContainsDate (DateSpan Nothing Nothing) _ = True
|
|
|
|
spanContainsDate (DateSpan Nothing (Just e)) d = d < e
|
|
|
|
spanContainsDate (DateSpan (Just b) Nothing) d = d >= b
|
|
|
|
spanContainsDate (DateSpan (Just b) (Just e)) d = d >= b && d < e
|
2014-09-11 00:07:53 +04:00
|
|
|
|
2016-12-03 02:36:23 +03:00
|
|
|
-- | Does the period include the given date ?
|
|
|
|
-- (Here to avoid import cycle).
|
|
|
|
periodContainsDate :: Period -> Day -> Bool
|
|
|
|
periodContainsDate p = spanContainsDate (periodAsDateSpan p)
|
|
|
|
|
2012-05-19 06:56:26 +04:00
|
|
|
-- | Calculate the intersection of a number of datespans.
|
|
|
|
spansIntersect [] = nulldatespan
|
|
|
|
spansIntersect [d] = d
|
|
|
|
spansIntersect (d:ds) = d `spanIntersect` (spansIntersect ds)
|
|
|
|
|
2011-09-23 04:09:39 +04:00
|
|
|
-- | Calculate the intersection of two datespans.
|
2018-03-29 20:09:15 +03:00
|
|
|
--
|
|
|
|
-- For non-intersecting spans, gives an empty span beginning on the second's start date:
|
|
|
|
-- >>> mkdatespan "2018-01-01" "2018-01-03" `spanIntersect` mkdatespan "2018-01-03" "2018-01-05"
|
|
|
|
-- DateSpan 2018/01/03-2018/01/02
|
2011-09-23 04:09:39 +04:00
|
|
|
spanIntersect (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan b e
|
|
|
|
where
|
|
|
|
b = latest b1 b2
|
|
|
|
e = earliest e1 e2
|
|
|
|
|
2014-04-19 19:38:03 +04:00
|
|
|
-- | Fill any unspecified dates in the first span with the dates from
|
|
|
|
-- the second one. Sort of a one-way spanIntersect.
|
|
|
|
spanDefaultsFrom (DateSpan a1 b1) (DateSpan a2 b2) = DateSpan a b
|
|
|
|
where a = if isJust a1 then a1 else a2
|
|
|
|
b = if isJust b1 then b1 else b2
|
|
|
|
|
2012-05-19 06:56:26 +04:00
|
|
|
-- | Calculate the union of a number of datespans.
|
|
|
|
spansUnion [] = nulldatespan
|
|
|
|
spansUnion [d] = d
|
|
|
|
spansUnion (d:ds) = d `spanUnion` (spansUnion ds)
|
|
|
|
|
|
|
|
-- | Calculate the union of two datespans.
|
|
|
|
spanUnion (DateSpan b1 e1) (DateSpan b2 e2) = DateSpan b e
|
|
|
|
where
|
|
|
|
b = earliest b1 b2
|
|
|
|
e = latest e1 e2
|
|
|
|
|
2011-09-23 04:09:39 +04:00
|
|
|
latest d Nothing = d
|
|
|
|
latest Nothing d = d
|
|
|
|
latest (Just d1) (Just d2) = Just $ max d1 d2
|
|
|
|
|
|
|
|
earliest d Nothing = d
|
|
|
|
earliest Nothing d = d
|
|
|
|
earliest (Just d1) (Just d2) = Just $ min d1 d2
|
|
|
|
|
2008-12-04 02:20:38 +03:00
|
|
|
-- | Parse a period expression to an Interval and overall DateSpan using
|
2010-08-01 04:15:21 +04:00
|
|
|
-- the provided reference date, or return a parse error.
|
2018-09-30 04:32:08 +03:00
|
|
|
parsePeriodExpr
|
|
|
|
:: Day -> Text -> Either (ParseErrorBundle Text CustomErr) (Interval, DateSpan)
|
2018-06-09 05:35:27 +03:00
|
|
|
parsePeriodExpr refdate s = parsewith (periodexprp refdate <* eof) (T.toLower s)
|
|
|
|
|
|
|
|
-- | Like parsePeriodExpr, but call error' on failure.
|
|
|
|
parsePeriodExpr' :: Day -> Text -> (Interval, DateSpan)
|
|
|
|
parsePeriodExpr' refdate s =
|
2018-09-30 04:32:08 +03:00
|
|
|
either (error' . ("failed to parse:" ++) . customErrorBundlePretty) id $
|
2018-06-09 05:35:27 +03:00
|
|
|
parsePeriodExpr refdate s
|
2010-08-01 04:15:21 +04:00
|
|
|
|
2016-07-29 18:57:10 +03:00
|
|
|
maybePeriod :: Day -> Text -> Maybe (Interval,DateSpan)
|
2011-08-16 02:50:09 +04:00
|
|
|
maybePeriod refdate = either (const Nothing) Just . parsePeriodExpr refdate
|
|
|
|
|
2010-08-01 04:15:21 +04:00
|
|
|
-- | Show a DateSpan as a human-readable pseudo-period-expression string.
|
2012-05-07 00:42:34 +04:00
|
|
|
-- dateSpanAsText :: DateSpan -> String
|
|
|
|
-- dateSpanAsText (DateSpan Nothing Nothing) = "all"
|
|
|
|
-- dateSpanAsText (DateSpan Nothing (Just e)) = printf "to %s" (show e)
|
|
|
|
-- dateSpanAsText (DateSpan (Just b) Nothing) = printf "from %s" (show b)
|
|
|
|
-- dateSpanAsText (DateSpan (Just b) (Just e)) = printf "%s to %s" (show b) (show e)
|
2014-09-11 00:07:53 +04:00
|
|
|
|
2008-12-04 02:20:38 +03:00
|
|
|
-- | Convert a single smart date string to a date span using the provided
|
2010-03-09 20:38:12 +03:00
|
|
|
-- reference date, or raise an error.
|
2012-05-07 00:42:34 +04:00
|
|
|
-- spanFromSmartDateString :: Day -> String -> DateSpan
|
|
|
|
-- spanFromSmartDateString refdate s = spanFromSmartDate refdate sdate
|
|
|
|
-- where
|
|
|
|
-- sdate = fromparse $ parsewith smartdateonly s
|
2008-11-27 22:42:03 +03:00
|
|
|
|
|
|
|
spanFromSmartDate :: Day -> SmartDate -> DateSpan
|
|
|
|
spanFromSmartDate refdate sdate = DateSpan (Just b) (Just e)
|
|
|
|
where
|
2009-06-05 13:44:20 +04:00
|
|
|
(ry,rm,_) = toGregorian refdate
|
2008-11-27 09:29:29 +03:00
|
|
|
(b,e) = span sdate
|
|
|
|
span :: SmartDate -> (Day,Day)
|
|
|
|
span ("","","today") = (refdate, nextday refdate)
|
|
|
|
span ("","this","day") = (refdate, nextday refdate)
|
|
|
|
span ("","","yesterday") = (prevday refdate, refdate)
|
|
|
|
span ("","last","day") = (prevday refdate, refdate)
|
|
|
|
span ("","","tomorrow") = (nextday refdate, addDays 2 refdate)
|
|
|
|
span ("","next","day") = (nextday refdate, addDays 2 refdate)
|
|
|
|
span ("","last","week") = (prevweek refdate, thisweek refdate)
|
|
|
|
span ("","this","week") = (thisweek refdate, nextweek refdate)
|
|
|
|
span ("","next","week") = (nextweek refdate, startofweek $ addDays 14 refdate)
|
|
|
|
span ("","last","month") = (prevmonth refdate, thismonth refdate)
|
|
|
|
span ("","this","month") = (thismonth refdate, nextmonth refdate)
|
|
|
|
span ("","next","month") = (nextmonth refdate, startofmonth $ addGregorianMonthsClip 2 refdate)
|
|
|
|
span ("","last","quarter") = (prevquarter refdate, thisquarter refdate)
|
|
|
|
span ("","this","quarter") = (thisquarter refdate, nextquarter refdate)
|
|
|
|
span ("","next","quarter") = (nextquarter refdate, startofquarter $ addGregorianMonthsClip 6 refdate)
|
|
|
|
span ("","last","year") = (prevyear refdate, thisyear refdate)
|
|
|
|
span ("","this","year") = (thisyear refdate, nextyear refdate)
|
|
|
|
span ("","next","year") = (nextyear refdate, startofyear $ addGregorianYearsClip 2 refdate)
|
|
|
|
span ("","",d) = (day, nextday day) where day = fromGregorian ry rm (read d)
|
|
|
|
span ("",m,"") = (startofmonth day, nextmonth day) where day = fromGregorian ry (read m) 1
|
|
|
|
span ("",m,d) = (day, nextday day) where day = fromGregorian ry (read m) (read d)
|
|
|
|
span (y,"","") = (startofyear day, nextyear day) where day = fromGregorian (read y) 1 1
|
|
|
|
span (y,m,"") = (startofmonth day, nextmonth day) where day = fromGregorian (read y) (read m) 1
|
|
|
|
span (y,m,d) = (day, nextday day) where day = fromGregorian (read y) (read m) (read d)
|
|
|
|
|
2012-05-07 00:42:34 +04:00
|
|
|
-- showDay :: Day -> String
|
|
|
|
-- showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day
|
2010-03-09 21:33:26 +03:00
|
|
|
|
2009-12-13 01:19:57 +03:00
|
|
|
-- | Convert a smart date string to an explicit yyyy\/mm\/dd string using
|
2010-03-09 20:38:12 +03:00
|
|
|
-- the provided reference date, or raise an error.
|
2016-07-29 18:57:10 +03:00
|
|
|
fixSmartDateStr :: Day -> Text -> String
|
2019-02-14 16:14:52 +03:00
|
|
|
fixSmartDateStr d s =
|
|
|
|
either (error' . printf "could not parse date %s %s" (show s) . show) id $
|
|
|
|
(fixSmartDateStrEither d s :: Either (ParseErrorBundle Text CustomErr) String)
|
2010-03-09 21:33:26 +03:00
|
|
|
|
|
|
|
-- | A safe version of fixSmartDateStr.
|
2018-09-30 04:32:08 +03:00
|
|
|
fixSmartDateStrEither :: Day -> Text -> Either (ParseErrorBundle Text CustomErr) String
|
2019-02-14 16:14:52 +03:00
|
|
|
fixSmartDateStrEither d = fmap showDate . fixSmartDateStrEither' d
|
2011-08-15 02:39:48 +04:00
|
|
|
|
2018-09-30 04:32:08 +03:00
|
|
|
fixSmartDateStrEither'
|
|
|
|
:: Day -> Text -> Either (ParseErrorBundle Text CustomErr) Day
|
2016-07-29 18:57:10 +03:00
|
|
|
fixSmartDateStrEither' d s = case parsewith smartdateonly (T.toLower s) of
|
2011-08-15 02:39:48 +04:00
|
|
|
Right sd -> Right $ fixSmartDate d sd
|
|
|
|
Left e -> Left e
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2008-11-27 09:48:46 +03:00
|
|
|
-- | Convert a SmartDate to an absolute date using the provided reference date.
|
2016-05-07 04:05:42 +03:00
|
|
|
--
|
|
|
|
-- ==== Examples:
|
2016-12-29 22:15:01 +03:00
|
|
|
-- >>> :set -XOverloadedStrings
|
2016-05-07 04:05:42 +03:00
|
|
|
-- >>> let t = fixSmartDateStr (parsedate "2008/11/26")
|
|
|
|
-- >>> t "0000-01-01"
|
|
|
|
-- "0000/01/01"
|
|
|
|
-- >>> t "1999-12-02"
|
|
|
|
-- "1999/12/02"
|
|
|
|
-- >>> t "1999.12.02"
|
|
|
|
-- "1999/12/02"
|
|
|
|
-- >>> t "1999/3/2"
|
|
|
|
-- "1999/03/02"
|
|
|
|
-- >>> t "19990302"
|
|
|
|
-- "1999/03/02"
|
|
|
|
-- >>> t "2008/2"
|
|
|
|
-- "2008/02/01"
|
|
|
|
-- >>> t "0020/2"
|
|
|
|
-- "0020/02/01"
|
|
|
|
-- >>> t "1000"
|
|
|
|
-- "1000/01/01"
|
|
|
|
-- >>> t "4/2"
|
|
|
|
-- "2008/04/02"
|
|
|
|
-- >>> t "2"
|
|
|
|
-- "2008/11/02"
|
|
|
|
-- >>> t "January"
|
|
|
|
-- "2008/01/01"
|
|
|
|
-- >>> t "feb"
|
|
|
|
-- "2008/02/01"
|
|
|
|
-- >>> t "today"
|
|
|
|
-- "2008/11/26"
|
|
|
|
-- >>> t "yesterday"
|
|
|
|
-- "2008/11/25"
|
|
|
|
-- >>> t "tomorrow"
|
|
|
|
-- "2008/11/27"
|
|
|
|
-- >>> t "this day"
|
|
|
|
-- "2008/11/26"
|
|
|
|
-- >>> t "last day"
|
|
|
|
-- "2008/11/25"
|
|
|
|
-- >>> t "next day"
|
|
|
|
-- "2008/11/27"
|
|
|
|
-- >>> t "this week" -- last monday
|
|
|
|
-- "2008/11/24"
|
|
|
|
-- >>> t "last week" -- previous monday
|
|
|
|
-- "2008/11/17"
|
|
|
|
-- >>> t "next week" -- next monday
|
|
|
|
-- "2008/12/01"
|
|
|
|
-- >>> t "this month"
|
|
|
|
-- "2008/11/01"
|
|
|
|
-- >>> t "last month"
|
|
|
|
-- "2008/10/01"
|
|
|
|
-- >>> t "next month"
|
|
|
|
-- "2008/12/01"
|
|
|
|
-- >>> t "this quarter"
|
|
|
|
-- "2008/10/01"
|
|
|
|
-- >>> t "last quarter"
|
|
|
|
-- "2008/07/01"
|
|
|
|
-- >>> t "next quarter"
|
|
|
|
-- "2009/01/01"
|
|
|
|
-- >>> t "this year"
|
|
|
|
-- "2008/01/01"
|
|
|
|
-- >>> t "last year"
|
|
|
|
-- "2007/01/01"
|
|
|
|
-- >>> t "next year"
|
|
|
|
-- "2009/01/01"
|
|
|
|
--
|
|
|
|
-- t "last wed"
|
|
|
|
-- "2008/11/19"
|
|
|
|
-- t "next friday"
|
|
|
|
-- "2008/11/28"
|
|
|
|
-- t "next january"
|
|
|
|
-- "2009/01/01"
|
|
|
|
--
|
2008-11-27 07:01:07 +03:00
|
|
|
fixSmartDate :: Day -> SmartDate -> Day
|
2019-02-14 16:14:52 +03:00
|
|
|
fixSmartDate refdate = fix
|
|
|
|
where
|
|
|
|
fix :: SmartDate -> Day
|
|
|
|
fix ("", "", "today") = fromGregorian ry rm rd
|
|
|
|
fix ("", "this", "day") = fromGregorian ry rm rd
|
|
|
|
fix ("", "", "yesterday") = prevday refdate
|
|
|
|
fix ("", "last", "day") = prevday refdate
|
|
|
|
fix ("", "", "tomorrow") = nextday refdate
|
|
|
|
fix ("", "next", "day") = nextday refdate
|
|
|
|
fix ("", "last", "week") = prevweek refdate
|
|
|
|
fix ("", "this", "week") = thisweek refdate
|
|
|
|
fix ("", "next", "week") = nextweek refdate
|
|
|
|
fix ("", "last", "month") = prevmonth refdate
|
|
|
|
fix ("", "this", "month") = thismonth refdate
|
|
|
|
fix ("", "next", "month") = nextmonth refdate
|
|
|
|
fix ("", "last", "quarter") = prevquarter refdate
|
|
|
|
fix ("", "this", "quarter") = thisquarter refdate
|
|
|
|
fix ("", "next", "quarter") = nextquarter refdate
|
|
|
|
fix ("", "last", "year") = prevyear refdate
|
|
|
|
fix ("", "this", "year") = thisyear refdate
|
|
|
|
fix ("", "next", "year") = nextyear refdate
|
|
|
|
fix ("", "", d) = fromGregorian ry rm (read d)
|
|
|
|
fix ("", m, "") = fromGregorian ry (read m) 1
|
|
|
|
fix ("", m, d) = fromGregorian ry (read m) (read d)
|
|
|
|
fix (y, "", "") = fromGregorian (read y) 1 1
|
|
|
|
fix (y, m, "") = fromGregorian (read y) (read m) 1
|
|
|
|
fix (y, m, d) = fromGregorian (read y) (read m) (read d)
|
|
|
|
(ry, rm, rd) = toGregorian refdate
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2008-11-27 05:49:22 +03:00
|
|
|
prevday :: Day -> Day
|
|
|
|
prevday = addDays (-1)
|
|
|
|
nextday = addDays 1
|
2008-12-04 02:20:38 +03:00
|
|
|
startofday = id
|
2008-11-27 04:49:13 +03:00
|
|
|
|
2008-11-27 05:49:22 +03:00
|
|
|
thisweek = startofweek
|
|
|
|
prevweek = startofweek . addDays (-7)
|
|
|
|
nextweek = startofweek . addDays 7
|
|
|
|
startofweek day = fromMondayStartWeek y w 1
|
2008-11-27 04:49:13 +03:00
|
|
|
where
|
2008-11-27 05:49:22 +03:00
|
|
|
(y,_,_) = toGregorian day
|
2008-11-27 04:49:13 +03:00
|
|
|
(w,_) = mondayStartWeek day
|
|
|
|
|
2008-11-27 05:49:22 +03:00
|
|
|
thismonth = startofmonth
|
|
|
|
prevmonth = startofmonth . addGregorianMonthsClip (-1)
|
|
|
|
nextmonth = startofmonth . addGregorianMonthsClip 1
|
|
|
|
startofmonth day = fromGregorian y m 1 where (y,m,_) = toGregorian day
|
2017-11-25 01:44:10 +03:00
|
|
|
nthdayofmonth d day = fromGregorian y m d where (y,m,_) = toGregorian day
|
2008-11-27 05:49:22 +03:00
|
|
|
|
|
|
|
thisquarter = startofquarter
|
|
|
|
prevquarter = startofquarter . addGregorianMonthsClip (-3)
|
|
|
|
nextquarter = startofquarter . addGregorianMonthsClip 3
|
|
|
|
startofquarter day = fromGregorian y (firstmonthofquarter m) 1
|
|
|
|
where
|
|
|
|
(y,m,_) = toGregorian day
|
|
|
|
firstmonthofquarter m = ((m-1) `div` 3) * 3 + 1
|
|
|
|
|
|
|
|
thisyear = startofyear
|
|
|
|
prevyear = startofyear . addGregorianYearsClip (-1)
|
|
|
|
nextyear = startofyear . addGregorianYearsClip 1
|
|
|
|
startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2018-07-30 13:05:38 +03:00
|
|
|
-- | For given date d find year-long interval that starts on given
|
|
|
|
-- MM/DD of year and covers it.
|
|
|
|
-- The given MM and DD should be basically valid (1-12 & 1-31),
|
|
|
|
-- or an error is raised.
|
2017-11-25 01:43:53 +03:00
|
|
|
--
|
|
|
|
-- Examples: lets take 2017-11-22. Year-long intervals covering it that
|
|
|
|
-- starts before Nov 22 will start in 2017. However
|
|
|
|
-- intervals that start after Nov 23rd should start in 2016:
|
2019-07-15 13:28:52 +03:00
|
|
|
-- >>> let wed22nd = parsedate "2017-11-22"
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> nthdayofyearcontaining 11 21 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-21
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> nthdayofyearcontaining 11 22 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-22
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> nthdayofyearcontaining 11 23 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2016-11-23
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> nthdayofyearcontaining 12 02 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2016-12-02
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> nthdayofyearcontaining 12 31 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2016-12-31
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> nthdayofyearcontaining 1 1 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-01-01
|
2018-07-24 16:24:45 +03:00
|
|
|
nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day
|
2018-07-30 13:05:38 +03:00
|
|
|
nthdayofyearcontaining m md date
|
|
|
|
| not (validMonth $ show m) = error' $ "nthdayofyearcontaining: invalid month "++show m
|
|
|
|
| not (validDay $ show md) = error' $ "nthdayofyearcontaining: invalid day " ++show md
|
|
|
|
| mmddOfSameYear <= date = mmddOfSameYear
|
|
|
|
| otherwise = mmddOfPrevYear
|
|
|
|
where mmddOfSameYear = addDays (fromIntegral md-1) $ applyN (m-1) nextmonth s
|
|
|
|
mmddOfPrevYear = addDays (fromIntegral md-1) $ applyN (m-1) nextmonth $ prevyear s
|
|
|
|
s = startofyear date
|
2017-11-25 01:43:53 +03:00
|
|
|
|
2017-11-25 00:51:51 +03:00
|
|
|
-- | For given date d find month-long interval that starts on nth day of month
|
2019-07-15 13:28:52 +03:00
|
|
|
-- and covers it.
|
2018-07-30 13:05:38 +03:00
|
|
|
-- The given day of month should be basically valid (1-31), or an error is raised.
|
2017-11-25 00:51:51 +03:00
|
|
|
--
|
|
|
|
-- Examples: lets take 2017-11-22. Month-long intervals covering it that
|
|
|
|
-- start on 1st-22nd of month will start in Nov. However
|
|
|
|
-- intervals that start on 23rd-30th of month should start in Oct:
|
2019-07-15 13:28:52 +03:00
|
|
|
-- >>> let wed22nd = parsedate "2017-11-22"
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofmonthcontaining 1 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-01
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofmonthcontaining 12 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-12
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofmonthcontaining 22 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-22
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofmonthcontaining 23 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-10-23
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofmonthcontaining 30 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-10-30
|
2018-07-24 16:24:45 +03:00
|
|
|
nthdayofmonthcontaining :: MonthDay -> Day -> Day
|
2018-07-30 13:05:38 +03:00
|
|
|
nthdayofmonthcontaining md date
|
|
|
|
| not (validDay $ show md) = error' $ "nthdayofmonthcontaining: invalid day " ++show md
|
|
|
|
| nthOfSameMonth <= date = nthOfSameMonth
|
|
|
|
| otherwise = nthOfPrevMonth
|
|
|
|
where nthOfSameMonth = nthdayofmonth md s
|
|
|
|
nthOfPrevMonth = nthdayofmonth md $ prevmonth s
|
|
|
|
s = startofmonth date
|
2011-01-14 07:32:08 +03:00
|
|
|
|
2017-11-25 00:51:51 +03:00
|
|
|
-- | For given date d find week-long interval that starts on nth day of week
|
2019-07-15 13:28:52 +03:00
|
|
|
-- and covers it.
|
2017-11-25 00:51:51 +03:00
|
|
|
--
|
|
|
|
-- Examples: 2017-11-22 is Wed. Week-long intervals that cover it and
|
|
|
|
-- start on Mon, Tue or Wed will start in the same week. However
|
2019-07-15 13:28:52 +03:00
|
|
|
-- intervals that start on Thu or Fri should start in prev week:
|
|
|
|
-- >>> let wed22nd = parsedate "2017-11-22"
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofweekcontaining 1 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-20
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofweekcontaining 2 wed22nd
|
|
|
|
-- 2017-11-21
|
|
|
|
-- >>> nthdayofweekcontaining 3 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-22
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofweekcontaining 4 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-16
|
2017-11-25 00:51:51 +03:00
|
|
|
-- >>> nthdayofweekcontaining 5 wed22nd
|
2019-07-15 13:28:52 +03:00
|
|
|
-- 2017-11-17
|
2018-07-24 16:24:45 +03:00
|
|
|
nthdayofweekcontaining :: WeekDay -> Day -> Day
|
2017-11-25 00:51:51 +03:00
|
|
|
nthdayofweekcontaining n d | nthOfSameWeek <= d = nthOfSameWeek
|
|
|
|
| otherwise = nthOfPrevWeek
|
|
|
|
where nthOfSameWeek = addDays (fromIntegral n-1) s
|
|
|
|
nthOfPrevWeek = addDays (fromIntegral n-1) $ prevweek s
|
2011-01-14 07:32:08 +03:00
|
|
|
s = startofweek d
|
|
|
|
|
2017-11-25 02:52:34 +03:00
|
|
|
-- | For given date d find month-long interval that starts on nth weekday of month
|
2019-07-15 13:28:52 +03:00
|
|
|
-- and covers it.
|
2017-11-25 02:52:34 +03:00
|
|
|
--
|
|
|
|
-- Examples: 2017-11-22 is 3rd Wed of Nov. Month-long intervals that cover it and
|
|
|
|
-- start on 1st-4th Wed will start in Nov. However
|
2019-07-15 13:28:52 +03:00
|
|
|
-- intervals that start on 4th Thu or Fri or later should start in Oct:
|
|
|
|
-- >>> let wed22nd = parsedate "2017-11-22"
|
2017-11-25 02:52:34 +03:00
|
|
|
-- >>> nthweekdayofmonthcontaining 1 3 wed22nd
|
|
|
|
-- 2017-11-01
|
|
|
|
-- >>> nthweekdayofmonthcontaining 3 2 wed22nd
|
|
|
|
-- 2017-11-21
|
|
|
|
-- >>> nthweekdayofmonthcontaining 4 3 wed22nd
|
|
|
|
-- 2017-11-22
|
|
|
|
-- >>> nthweekdayofmonthcontaining 4 4 wed22nd
|
|
|
|
-- 2017-10-26
|
|
|
|
-- >>> nthweekdayofmonthcontaining 4 5 wed22nd
|
|
|
|
-- 2017-10-27
|
2018-07-24 16:24:45 +03:00
|
|
|
nthweekdayofmonthcontaining :: Int -> WeekDay -> Day -> Day
|
2017-11-25 02:52:34 +03:00
|
|
|
nthweekdayofmonthcontaining n wd d | nthWeekdaySameMonth <= d = nthWeekdaySameMonth
|
|
|
|
| otherwise = nthWeekdayPrevMonth
|
|
|
|
where nthWeekdaySameMonth = advancetonthweekday n wd $ startofmonth d
|
|
|
|
nthWeekdayPrevMonth = advancetonthweekday n wd $ prevmonth d
|
2018-07-24 16:24:45 +03:00
|
|
|
|
2017-11-25 02:52:34 +03:00
|
|
|
-- | Advance to nth weekday wd after given start day s
|
2018-07-24 16:24:45 +03:00
|
|
|
advancetonthweekday :: Int -> WeekDay -> Day -> Day
|
2019-07-15 13:28:52 +03:00
|
|
|
advancetonthweekday n wd s =
|
2018-07-24 16:36:45 +03:00
|
|
|
maybe err (addWeeks (n-1)) $ firstMatch (>=s) $ iterate (addWeeks 1) $ firstweekday s
|
|
|
|
where
|
|
|
|
err = error' "advancetonthweekday: should not happen"
|
2017-11-25 02:52:34 +03:00
|
|
|
addWeeks k = addDays (7 * fromIntegral k)
|
2019-07-15 13:28:52 +03:00
|
|
|
firstMatch p = headMay . dropWhile (not . p)
|
2017-11-25 02:52:34 +03:00
|
|
|
firstweekday = addDays (fromIntegral wd-1) . startofweek
|
|
|
|
|
2008-11-27 03:35:00 +03:00
|
|
|
----------------------------------------------------------------------
|
|
|
|
-- parsing
|
|
|
|
|
2012-05-07 00:42:34 +04:00
|
|
|
-- -- | Parse a couple of date-time string formats to a time type.
|
|
|
|
-- parsedatetimeM :: String -> Maybe LocalTime
|
|
|
|
-- parsedatetimeM s = firstJust [
|
|
|
|
-- parseTime defaultTimeLocale "%Y/%m/%d %H:%M:%S" s,
|
|
|
|
-- parseTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" s
|
|
|
|
-- ]
|
2009-01-11 09:58:35 +03:00
|
|
|
|
2015-03-30 02:12:54 +03:00
|
|
|
parsetime :: ParseTime t => TimeLocale -> String -> String -> Maybe t
|
|
|
|
parsetime =
|
|
|
|
#if MIN_VERSION_time(1,5,0)
|
|
|
|
parseTimeM True
|
|
|
|
#else
|
|
|
|
parseTime
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
2010-03-09 20:38:12 +03:00
|
|
|
-- | Parse a couple of date string formats to a time type.
|
2009-01-11 09:58:35 +03:00
|
|
|
parsedateM :: String -> Maybe Day
|
2014-09-11 00:07:53 +04:00
|
|
|
parsedateM s = firstJust [
|
2015-03-30 02:12:54 +03:00
|
|
|
parsetime defaultTimeLocale "%Y/%m/%d" s,
|
|
|
|
parsetime defaultTimeLocale "%Y-%m-%d" s
|
2009-01-11 09:58:35 +03:00
|
|
|
]
|
2008-11-11 15:34:05 +03:00
|
|
|
|
2015-03-30 02:12:54 +03:00
|
|
|
|
2012-05-07 00:42:34 +04:00
|
|
|
-- -- | Parse a date-time string to a time type, or raise an error.
|
|
|
|
-- parsedatetime :: String -> LocalTime
|
|
|
|
-- parsedatetime s = fromMaybe (error' $ "could not parse timestamp \"" ++ s ++ "\"")
|
|
|
|
-- (parsedatetimeM s)
|
2010-03-09 20:38:12 +03:00
|
|
|
|
2016-05-07 05:02:48 +03:00
|
|
|
-- | Parse a YYYY-MM-DD or YYYY/MM/DD date string to a Day, or raise an error. For testing/debugging.
|
|
|
|
--
|
|
|
|
-- >>> parsedate "2008/02/03"
|
|
|
|
-- 2008-02-03
|
2008-11-27 07:01:07 +03:00
|
|
|
parsedate :: String -> Day
|
2010-09-05 22:18:50 +04:00
|
|
|
parsedate s = fromMaybe (error' $ "could not parse date \"" ++ s ++ "\"")
|
2009-01-11 09:58:35 +03:00
|
|
|
(parsedateM s)
|
2016-05-27 18:06:53 +03:00
|
|
|
-- doctests I haven't been able to make compatible with both GHC 7 and 8
|
|
|
|
-- -- >>> parsedate "2008/02/03/"
|
|
|
|
-- -- *** Exception: could not parse date "2008/02/03/"
|
|
|
|
-- #if MIN_VERSION_base(4,9,0)
|
|
|
|
-- -- ...
|
|
|
|
-- #endif
|
|
|
|
-- #if MIN_VERSION_time(1,6,0)
|
|
|
|
-- -- >>> parsedate "2008/02/30" -- with time >= 1.6, invalid dates are rejected
|
|
|
|
-- -- *** Exception: could not parse date "2008/02/30"
|
|
|
|
-- #if MIN_VERSION_base(4,9,0)
|
|
|
|
-- -- ...
|
|
|
|
-- #endif
|
|
|
|
-- #else
|
|
|
|
-- -- >>> parsedate "2008/02/30" -- with time < 1.6, they are silently adjusted
|
|
|
|
-- -- 2008-02-29
|
|
|
|
-- #endif
|
2008-11-11 15:34:05 +03:00
|
|
|
|
2014-09-11 00:07:53 +04:00
|
|
|
{-|
|
2018-04-04 19:45:23 +03:00
|
|
|
Parse a date in any of the formats allowed in Ledger's period expressions, and some others.
|
2008-11-27 03:35:00 +03:00
|
|
|
Assumes any text in the parse stream has been lowercased.
|
2018-04-04 19:45:23 +03:00
|
|
|
Returns a SmartDate, to be converted to a full date later (see fixSmartDate).
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
|
|
|
> 2004 (start of year, which must have 4+ digits)
|
|
|
|
> 2004/10 (start of month, which must be 1-12)
|
|
|
|
> 2004/10/1 (exact date, day must be 1-31)
|
|
|
|
> 10/1 (month and day in current year)
|
|
|
|
> 21 (day in current month)
|
|
|
|
> october, oct (start of month in current year)
|
|
|
|
> yesterday, today, tomorrow (-1, 0, 1 days from today)
|
|
|
|
> last/this/next day/week/month/quarter/year (-1, 0, 1 periods from the current period)
|
|
|
|
> 20181201 (8 digit YYYYMMDD with valid year month and day)
|
|
|
|
> 201812 (6 digit YYYYMM with valid year and month)
|
|
|
|
|
|
|
|
Note malformed digit sequences might give surprising results:
|
|
|
|
|
|
|
|
> 201813 (6 digits with an invalid month is parsed as start of 6-digit year)
|
|
|
|
> 20181301 (8 digits with an invalid month is parsed as start of 8-digit year)
|
|
|
|
> 20181232 (8 digits with an invalid day gives an error)
|
|
|
|
> 201801012 (9+ digits beginning with a valid YYYYMMDD gives an error)
|
|
|
|
|
|
|
|
Eg:
|
|
|
|
|
|
|
|
YYYYMMDD is parsed as year-month-date if those parts are valid
|
|
|
|
(>=4 digits, 1-12, and 1-31 respectively):
|
|
|
|
>>> parsewith (smartdate <* eof) "20181201"
|
|
|
|
Right ("2018","12","01")
|
|
|
|
|
|
|
|
YYYYMM is parsed as year-month-01 if year and month are valid:
|
|
|
|
>>> parsewith (smartdate <* eof) "201804"
|
|
|
|
Right ("2018","04","01")
|
|
|
|
|
|
|
|
With an invalid month, it's parsed as a year:
|
|
|
|
>>> parsewith (smartdate <* eof) "201813"
|
|
|
|
Right ("201813","","")
|
|
|
|
|
|
|
|
A 9+ digit number beginning with valid YYYYMMDD gives an error:
|
|
|
|
>>> parsewith (smartdate <* eof) "201801012"
|
2018-05-13 18:10:45 +03:00
|
|
|
Left (...)
|
2018-04-04 19:45:23 +03:00
|
|
|
|
|
|
|
Big numbers not beginning with a valid YYYYMMDD are parsed as a year:
|
|
|
|
>>> parsewith (smartdate <* eof) "201813012"
|
|
|
|
Right ("201813012","","")
|
|
|
|
|
2008-11-27 03:35:00 +03:00
|
|
|
-}
|
2018-06-12 22:29:22 +03:00
|
|
|
smartdate :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
smartdate = do
|
2010-09-04 03:22:58 +04:00
|
|
|
-- XXX maybe obscures date errors ? see ledgerdate
|
2018-04-04 19:45:23 +03:00
|
|
|
(y,m,d) <- choice' [yyyymmdd, yyyymm, ymd, ym, md, y, d, month, mon, today, yesterday, tomorrow, lastthisnextthing]
|
2009-09-22 15:55:11 +04:00
|
|
|
return (y,m,d)
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2010-03-10 02:11:12 +03:00
|
|
|
-- | Like smartdate, but there must be nothing other than whitespace after the date.
|
2018-06-12 22:29:22 +03:00
|
|
|
smartdateonly :: TextParser m SmartDate
|
2010-03-10 02:11:12 +03:00
|
|
|
smartdateonly = do
|
|
|
|
d <- smartdate
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2010-03-10 02:11:12 +03:00
|
|
|
eof
|
|
|
|
return d
|
|
|
|
|
2019-02-14 16:14:52 +03:00
|
|
|
datesepchars :: String
|
2010-09-04 03:22:58 +04:00
|
|
|
datesepchars = "/-."
|
2018-05-22 04:52:34 +03:00
|
|
|
|
2016-07-29 18:57:10 +03:00
|
|
|
datesepchar :: TextParser m Char
|
2018-05-22 04:52:34 +03:00
|
|
|
datesepchar = satisfy isDateSepChar
|
|
|
|
|
|
|
|
isDateSepChar :: Char -> Bool
|
|
|
|
isDateSepChar c = c == '/' || c == '-' || c == '.'
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2010-04-15 01:49:34 +04:00
|
|
|
validYear, validMonth, validDay :: String -> Bool
|
2015-07-12 04:44:32 +03:00
|
|
|
validYear s = length s >= 4 && isJust (readMay s :: Maybe Year)
|
2010-04-15 01:49:34 +04:00
|
|
|
validMonth s = maybe False (\n -> n>=1 && n<=12) $ readMay s
|
|
|
|
validDay s = maybe False (\n -> n>=1 && n<=31) $ readMay s
|
|
|
|
|
lib, cli, ui: start using Control.Monad.Fail, allow base-compat 0.11
fail is moving out of Monad and into it's own MonadFail class.
This will be enforced in GHC 8.8 (I think).
base-compat/base-compat-batteries 0.11.0 have adapted to this,
and are approaching stackage nightly
(https://github.com/commercialhaskell/stackage/issues/4802).
hledger is now ready to build with base-compat-batteries 0.11.0, once
all of our deps do (eg aeson). We are still compatible with the older
0.10.x and GHC 7.10.3 as well.
For now we are using both fails:
- new fail (from Control.Monad.Fail), used in our parsers, imported
via base-compat-batteries Control.Monad.Fail.Compat to work with
older GHC versions.
- old fail (from GHC.Base, exported by Prelude, Control.Monad,
Control.Monad.State.Strict, Prelude.Compat, ...), used in easytest's
Test, since I couldn't find their existing fail implementation to update.
To reduce (my) confusion, these are imported carefully, consistently,
and qualified everywhere as Fail.fail and Prelude.fail, with clashing
re-exports suppressed, like so:
import Prelude hiding (fail)
import qualified Prelude (fail)
import Control.Monad.State.Strict hiding (fail)
import "base-compat-batteries" Prelude.Compat hiding (fail)
import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail
2019-09-09 03:13:47 +03:00
|
|
|
failIfInvalidYear, failIfInvalidMonth, failIfInvalidDay :: (Fail.MonadFail m) => String -> m ()
|
|
|
|
failIfInvalidYear s = unless (validYear s) $ Fail.fail $ "bad year number: " ++ s
|
|
|
|
failIfInvalidMonth s = unless (validMonth s) $ Fail.fail $ "bad month number: " ++ s
|
|
|
|
failIfInvalidDay s = unless (validDay s) $ Fail.fail $ "bad day number: " ++ s
|
2010-04-15 01:49:34 +04:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
yyyymmdd :: TextParser m SmartDate
|
2009-01-17 23:07:24 +03:00
|
|
|
yyyymmdd = do
|
2016-07-29 18:57:10 +03:00
|
|
|
y <- count 4 digitChar
|
|
|
|
m <- count 2 digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidMonth m
|
2016-07-29 18:57:10 +03:00
|
|
|
d <- count 2 digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidDay d
|
2009-01-17 23:07:24 +03:00
|
|
|
return (y,m,d)
|
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
yyyymm :: TextParser m SmartDate
|
2018-04-04 19:45:23 +03:00
|
|
|
yyyymm = do
|
|
|
|
y <- count 4 digitChar
|
|
|
|
m <- count 2 digitChar
|
|
|
|
failIfInvalidMonth m
|
|
|
|
return (y,m,"01")
|
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
ymd :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
ymd = do
|
2016-07-29 18:57:10 +03:00
|
|
|
y <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidYear y
|
2014-08-08 04:36:10 +04:00
|
|
|
sep <- datesepchar
|
2016-07-29 18:57:10 +03:00
|
|
|
m <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidMonth m
|
2014-08-08 04:36:10 +04:00
|
|
|
char sep
|
2016-07-29 18:57:10 +03:00
|
|
|
d <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidDay d
|
2009-11-26 00:21:49 +03:00
|
|
|
return $ (y,m,d)
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
ym :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
ym = do
|
2016-07-29 18:57:10 +03:00
|
|
|
y <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidYear y
|
2008-11-27 03:35:00 +03:00
|
|
|
datesepchar
|
2016-07-29 18:57:10 +03:00
|
|
|
m <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidMonth m
|
2008-11-27 09:29:29 +03:00
|
|
|
return (y,m,"")
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
y :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
y = do
|
2016-07-29 18:57:10 +03:00
|
|
|
y <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidYear y
|
2008-11-27 09:29:29 +03:00
|
|
|
return (y,"","")
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
d :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
d = do
|
2016-07-29 18:57:10 +03:00
|
|
|
d <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidDay d
|
2008-11-27 03:35:00 +03:00
|
|
|
return ("","",d)
|
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
md :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
md = do
|
2016-07-29 18:57:10 +03:00
|
|
|
m <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidMonth m
|
2008-11-27 03:35:00 +03:00
|
|
|
datesepchar
|
2016-07-29 18:57:10 +03:00
|
|
|
d <- some digitChar
|
2010-04-15 01:49:34 +04:00
|
|
|
failIfInvalidDay d
|
2008-11-27 03:35:00 +03:00
|
|
|
return ("",m,d)
|
|
|
|
|
2019-07-15 13:28:52 +03:00
|
|
|
-- These are compared case insensitively, and should all be kept lower case.
|
2009-01-17 23:21:44 +03:00
|
|
|
months = ["january","february","march","april","may","june",
|
|
|
|
"july","august","september","october","november","december"]
|
|
|
|
monthabbrevs = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"]
|
2017-11-25 02:52:34 +03:00
|
|
|
weekdays = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]
|
|
|
|
weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]
|
2008-11-27 09:29:29 +03:00
|
|
|
|
2018-07-30 13:06:40 +03:00
|
|
|
-- | Convert a case insensitive english month name to a month number.
|
|
|
|
monthIndex name = maybe 0 (+1) $ T.toLower name `elemIndex` months
|
|
|
|
|
|
|
|
-- | Convert a case insensitive english three-letter month abbreviation to a month number.
|
|
|
|
monIndex name = maybe 0 (+1) $ T.toLower name `elemIndex` monthabbrevs
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
month :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
month = do
|
2018-06-12 22:29:22 +03:00
|
|
|
m <- choice $ map (try . string') months
|
2008-11-27 09:29:29 +03:00
|
|
|
let i = monthIndex m
|
2009-09-22 15:55:11 +04:00
|
|
|
return ("",show i,"")
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
mon :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
mon = do
|
2018-06-12 22:29:22 +03:00
|
|
|
m <- choice $ map (try . string') monthabbrevs
|
2008-11-27 09:29:29 +03:00
|
|
|
let i = monIndex m
|
|
|
|
return ("",show i,"")
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
weekday :: TextParser m Int
|
2017-11-25 02:52:34 +03:00
|
|
|
weekday = do
|
2018-07-24 16:36:45 +03:00
|
|
|
wday <- T.toLower <$> (choice . map string' $ weekdays ++ weekdayabbrevs)
|
|
|
|
case catMaybes $ [wday `elemIndex` weekdays, wday `elemIndex` weekdayabbrevs] of
|
|
|
|
(i:_) -> return (i+1)
|
lib, cli, ui: start using Control.Monad.Fail, allow base-compat 0.11
fail is moving out of Monad and into it's own MonadFail class.
This will be enforced in GHC 8.8 (I think).
base-compat/base-compat-batteries 0.11.0 have adapted to this,
and are approaching stackage nightly
(https://github.com/commercialhaskell/stackage/issues/4802).
hledger is now ready to build with base-compat-batteries 0.11.0, once
all of our deps do (eg aeson). We are still compatible with the older
0.10.x and GHC 7.10.3 as well.
For now we are using both fails:
- new fail (from Control.Monad.Fail), used in our parsers, imported
via base-compat-batteries Control.Monad.Fail.Compat to work with
older GHC versions.
- old fail (from GHC.Base, exported by Prelude, Control.Monad,
Control.Monad.State.Strict, Prelude.Compat, ...), used in easytest's
Test, since I couldn't find their existing fail implementation to update.
To reduce (my) confusion, these are imported carefully, consistently,
and qualified everywhere as Fail.fail and Prelude.fail, with clashing
re-exports suppressed, like so:
import Prelude hiding (fail)
import qualified Prelude (fail)
import Control.Monad.State.Strict hiding (fail)
import "base-compat-batteries" Prelude.Compat hiding (fail)
import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail
2019-09-09 03:13:47 +03:00
|
|
|
[] -> Fail.fail $ "weekday: should not happen: attempted to find " <>
|
|
|
|
show wday <> " in " <> show (weekdays ++ weekdayabbrevs)
|
2017-11-25 02:52:34 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
today,yesterday,tomorrow :: TextParser m SmartDate
|
|
|
|
today = string' "today" >> return ("","","today")
|
|
|
|
yesterday = string' "yesterday" >> return ("","","yesterday")
|
|
|
|
tomorrow = string' "tomorrow" >> return ("","","tomorrow")
|
2008-11-27 03:35:00 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
lastthisnextthing :: TextParser m SmartDate
|
2008-11-27 03:35:00 +03:00
|
|
|
lastthisnextthing = do
|
2018-06-12 22:29:22 +03:00
|
|
|
r <- choice $ map string' [
|
2017-07-27 14:59:55 +03:00
|
|
|
"last"
|
|
|
|
,"this"
|
|
|
|
,"next"
|
2008-11-27 03:35:00 +03:00
|
|
|
]
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline -- make the space optional for easier scripting
|
2018-06-12 22:29:22 +03:00
|
|
|
p <- choice $ map string' [
|
2017-07-27 14:59:55 +03:00
|
|
|
"day"
|
|
|
|
,"week"
|
|
|
|
,"month"
|
|
|
|
,"quarter"
|
|
|
|
,"year"
|
2008-11-27 03:35:00 +03:00
|
|
|
]
|
2009-01-17 23:21:44 +03:00
|
|
|
-- XXX support these in fixSmartDate
|
2018-06-12 22:29:22 +03:00
|
|
|
-- ++ (map string' $ months ++ monthabbrevs ++ weekdays ++ weekdayabbrevs)
|
2014-09-11 00:07:53 +04:00
|
|
|
|
2017-07-27 14:59:55 +03:00
|
|
|
return ("", T.unpack r, T.unpack p)
|
2008-11-22 15:18:19 +03:00
|
|
|
|
2019-07-25 12:46:45 +03:00
|
|
|
-- | Parse a period expression, specifying a date span and optionally
|
|
|
|
-- a reporting interval. Requires a reference "today" date for
|
|
|
|
-- resolving any relative start/end dates (only; it is not needed for
|
|
|
|
-- parsing the reporting interval).
|
|
|
|
--
|
2017-11-26 06:58:53 +03:00
|
|
|
-- >>> let p = parsePeriodExpr (parsedate "2008/11/26")
|
2017-11-25 03:42:39 +03:00
|
|
|
-- >>> p "from Aug to Oct"
|
2016-05-07 04:05:42 +03:00
|
|
|
-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
|
|
|
|
-- >>> p "aug to oct"
|
|
|
|
-- Right (NoInterval,DateSpan 2008/08/01-2008/09/30)
|
2017-11-25 03:42:39 +03:00
|
|
|
-- >>> p "every 3 days in Aug"
|
2016-05-07 04:05:42 +03:00
|
|
|
-- Right (Days 3,DateSpan 2008/08)
|
|
|
|
-- >>> p "daily from aug"
|
|
|
|
-- Right (Days 1,DateSpan 2008/08/01-)
|
|
|
|
-- >>> p "every week to 2009"
|
|
|
|
-- Right (Weeks 1,DateSpan -2008/12/31)
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> p "every 2nd day of month"
|
|
|
|
-- Right (DayOfMonth 2,DateSpan -)
|
|
|
|
-- >>> p "every 2nd day"
|
|
|
|
-- Right (DayOfMonth 2,DateSpan -)
|
|
|
|
-- >>> p "every 2nd day 2009-"
|
2019-07-15 13:28:52 +03:00
|
|
|
-- Right (DayOfMonth 2,DateSpan 2009/01/01-)
|
2017-11-25 01:43:53 +03:00
|
|
|
-- >>> p "every 29th Nov"
|
|
|
|
-- Right (DayOfYear 11 29,DateSpan -)
|
|
|
|
-- >>> p "every 29th nov -2009"
|
|
|
|
-- Right (DayOfYear 11 29,DateSpan -2008/12/31)
|
|
|
|
-- >>> p "every nov 29th"
|
|
|
|
-- Right (DayOfYear 11 29,DateSpan -)
|
|
|
|
-- >>> p "every Nov 29th 2009-"
|
|
|
|
-- Right (DayOfYear 11 29,DateSpan 2009/01/01-)
|
|
|
|
-- >>> p "every 11/29 from 2009"
|
|
|
|
-- Right (DayOfYear 11 29,DateSpan 2009/01/01-)
|
2017-11-25 02:52:34 +03:00
|
|
|
-- >>> p "every 2nd Thursday of month to 2009"
|
|
|
|
-- Right (WeekdayOfMonth 2 4,DateSpan -2008/12/31)
|
|
|
|
-- >>> p "every 1st monday of month to 2009"
|
|
|
|
-- Right (WeekdayOfMonth 1 1,DateSpan -2008/12/31)
|
2017-11-25 02:02:55 +03:00
|
|
|
-- >>> p "every tue"
|
|
|
|
-- Right (DayOfWeek 2,DateSpan -)
|
|
|
|
-- >>> p "every 2nd day of week"
|
|
|
|
-- Right (DayOfWeek 2,DateSpan -)
|
2017-11-26 06:58:53 +03:00
|
|
|
-- >>> p "every 2nd day of month"
|
|
|
|
-- Right (DayOfMonth 2,DateSpan -)
|
|
|
|
-- >>> p "every 2nd day"
|
|
|
|
-- Right (DayOfMonth 2,DateSpan -)
|
2017-11-25 02:02:55 +03:00
|
|
|
-- >>> p "every 2nd day 2009-"
|
|
|
|
-- Right (DayOfMonth 2,DateSpan 2009/01/01-)
|
2017-11-26 06:58:53 +03:00
|
|
|
-- >>> p "every 2nd day of month 2009-"
|
|
|
|
-- Right (DayOfMonth 2,DateSpan 2009/01/01-)
|
2018-06-12 22:29:22 +03:00
|
|
|
periodexprp :: Day -> TextParser m (Interval, DateSpan)
|
2018-06-13 02:39:17 +03:00
|
|
|
periodexprp rdate = do
|
|
|
|
skipMany spacenonewline
|
|
|
|
choice $ map try [
|
2018-06-09 05:35:27 +03:00
|
|
|
intervalanddateperiodexprp rdate,
|
|
|
|
(,) NoInterval <$> periodexprdatespanp rdate
|
2008-12-04 02:20:38 +03:00
|
|
|
]
|
2008-11-27 22:42:03 +03:00
|
|
|
|
2019-07-25 12:46:45 +03:00
|
|
|
-- Parse a reporting interval and a date span.
|
2018-06-12 22:29:22 +03:00
|
|
|
intervalanddateperiodexprp :: Day -> TextParser m (Interval, DateSpan)
|
2018-06-09 05:35:27 +03:00
|
|
|
intervalanddateperiodexprp rdate = do
|
|
|
|
i <- reportingintervalp
|
2017-11-26 06:58:53 +03:00
|
|
|
s <- option def . try $ do
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2018-06-09 05:35:27 +03:00
|
|
|
periodexprdatespanp rdate
|
2008-12-04 02:20:38 +03:00
|
|
|
return (i,s)
|
|
|
|
|
2011-01-14 07:32:08 +03:00
|
|
|
-- Parse a reporting interval.
|
2018-06-12 22:29:22 +03:00
|
|
|
reportingintervalp :: TextParser m Interval
|
2018-06-09 05:35:27 +03:00
|
|
|
reportingintervalp = choice' [
|
2011-01-14 05:35:00 +03:00
|
|
|
tryinterval "day" "daily" Days,
|
|
|
|
tryinterval "week" "weekly" Weeks,
|
|
|
|
tryinterval "month" "monthly" Months,
|
|
|
|
tryinterval "quarter" "quarterly" Quarters,
|
|
|
|
tryinterval "year" "yearly" Years,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "biweekly"
|
2011-01-14 05:35:00 +03:00
|
|
|
return $ Weeks 2,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "bimonthly"
|
2011-01-14 07:32:08 +03:00
|
|
|
return $ Months 2,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "every"
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2017-11-25 01:43:53 +03:00
|
|
|
n <- nth
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2018-06-12 22:29:22 +03:00
|
|
|
string' "day"
|
2017-11-25 01:43:53 +03:00
|
|
|
of_ "week"
|
2011-01-14 07:32:08 +03:00
|
|
|
return $ DayOfWeek n,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "every"
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2019-02-14 16:14:52 +03:00
|
|
|
DayOfWeek <$> weekday,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "every"
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2017-11-25 01:43:53 +03:00
|
|
|
n <- nth
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2018-06-12 22:29:22 +03:00
|
|
|
string' "day"
|
2017-11-25 01:43:53 +03:00
|
|
|
optOf_ "month"
|
|
|
|
return $ DayOfMonth n,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "every"
|
2017-11-25 01:43:53 +03:00
|
|
|
let mnth = choice' [month, mon] >>= \(_,m,_) -> return (read m)
|
2018-09-30 04:32:08 +03:00
|
|
|
d_o_y <- runPermutation $
|
|
|
|
DayOfYear <$> toPermutation (try (skipMany spacenonewline *> mnth))
|
|
|
|
<*> toPermutation (try (skipMany spacenonewline *> nth))
|
2017-11-25 01:43:53 +03:00
|
|
|
optOf_ "year"
|
|
|
|
return d_o_y,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "every"
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2017-11-25 01:43:53 +03:00
|
|
|
("",m,d) <- md
|
|
|
|
optOf_ "year"
|
2017-11-25 02:52:34 +03:00
|
|
|
return $ DayOfYear (read m) (read d),
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "every"
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2017-11-25 02:52:34 +03:00
|
|
|
n <- nth
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2017-11-25 02:52:34 +03:00
|
|
|
wd <- weekday
|
|
|
|
optOf_ "month"
|
|
|
|
return $ WeekdayOfMonth n wd
|
2011-01-14 07:32:08 +03:00
|
|
|
]
|
2008-12-04 02:20:38 +03:00
|
|
|
where
|
2017-11-25 01:43:53 +03:00
|
|
|
of_ period = do
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2018-06-12 22:29:22 +03:00
|
|
|
string' "of"
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2018-06-12 22:29:22 +03:00
|
|
|
string' period
|
2019-07-15 13:28:52 +03:00
|
|
|
|
2017-11-25 01:43:53 +03:00
|
|
|
optOf_ period = optional $ try $ of_ period
|
2019-07-15 13:28:52 +03:00
|
|
|
|
2017-11-25 01:43:53 +03:00
|
|
|
nth = do n <- some digitChar
|
2018-06-12 22:29:22 +03:00
|
|
|
choice' $ map string' ["st","nd","rd","th"]
|
2017-11-25 01:43:53 +03:00
|
|
|
return $ read n
|
2011-01-14 07:32:08 +03:00
|
|
|
|
|
|
|
-- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".
|
2018-06-12 22:29:22 +03:00
|
|
|
tryinterval :: String -> String -> (Int -> Interval) -> TextParser m Interval
|
2011-01-14 05:35:00 +03:00
|
|
|
tryinterval singular compact intcons =
|
2017-07-27 14:59:55 +03:00
|
|
|
choice' [
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' compact'
|
2017-07-27 14:59:55 +03:00
|
|
|
return $ intcons 1,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "every"
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2018-06-12 22:29:22 +03:00
|
|
|
string' singular'
|
2017-07-27 14:59:55 +03:00
|
|
|
return $ intcons 1,
|
2018-06-12 22:29:22 +03:00
|
|
|
do string' "every"
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2019-02-14 16:14:52 +03:00
|
|
|
n <- read <$> some digitChar
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2018-06-12 22:29:22 +03:00
|
|
|
string' plural'
|
2017-07-27 14:59:55 +03:00
|
|
|
return $ intcons n
|
|
|
|
]
|
|
|
|
where
|
|
|
|
compact' = T.pack compact
|
|
|
|
singular' = T.pack singular
|
|
|
|
plural' = T.pack $ singular ++ "s"
|
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
periodexprdatespanp :: Day -> TextParser m DateSpan
|
2018-06-09 05:35:27 +03:00
|
|
|
periodexprdatespanp rdate = choice $ map try [
|
|
|
|
doubledatespanp rdate,
|
|
|
|
fromdatespanp rdate,
|
|
|
|
todatespanp rdate,
|
|
|
|
justdatespanp rdate
|
2008-12-04 02:20:38 +03:00
|
|
|
]
|
|
|
|
|
2018-04-04 19:45:23 +03:00
|
|
|
-- |
|
|
|
|
-- -- >>> parsewith (doubledatespan (parsedate "2018/01/01") <* eof) "20180101-201804"
|
|
|
|
-- Right DateSpan 2018/01/01-2018/04/01
|
2018-06-12 22:29:22 +03:00
|
|
|
doubledatespanp :: Day -> TextParser m DateSpan
|
2018-06-09 05:35:27 +03:00
|
|
|
doubledatespanp rdate = do
|
2018-06-12 22:29:22 +03:00
|
|
|
optional (string' "from" >> skipMany spacenonewline)
|
2008-12-04 02:20:38 +03:00
|
|
|
b <- smartdate
|
2018-03-25 16:53:44 +03:00
|
|
|
skipMany spacenonewline
|
2018-06-12 22:29:22 +03:00
|
|
|
optional (choice [string' "to", string' "-"] >> skipMany spacenonewline)
|
2019-02-14 16:14:52 +03:00
|
|
|
DateSpan (Just $ fixSmartDate rdate b) . Just . fixSmartDate rdate <$> smartdate
|
2008-11-27 22:42:03 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
fromdatespanp :: Day -> TextParser m DateSpan
|
2018-06-09 05:35:27 +03:00
|
|
|
fromdatespanp rdate = do
|
2013-09-27 02:06:48 +04:00
|
|
|
b <- choice [
|
|
|
|
do
|
2018-06-12 22:29:22 +03:00
|
|
|
string' "from" >> skipMany spacenonewline
|
2013-09-27 02:06:48 +04:00
|
|
|
smartdate
|
|
|
|
,
|
|
|
|
do
|
|
|
|
d <- smartdate
|
2018-06-12 22:29:22 +03:00
|
|
|
string' "-"
|
2013-09-27 02:06:48 +04:00
|
|
|
return d
|
|
|
|
]
|
2008-12-04 02:20:38 +03:00
|
|
|
return $ DateSpan (Just $ fixSmartDate rdate b) Nothing
|
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
todatespanp :: Day -> TextParser m DateSpan
|
2018-06-09 05:35:27 +03:00
|
|
|
todatespanp rdate = do
|
2018-06-12 22:29:22 +03:00
|
|
|
choice [string' "to", string' "-"] >> skipMany spacenonewline
|
2019-02-14 16:14:52 +03:00
|
|
|
DateSpan Nothing . Just . fixSmartDate rdate <$> smartdate
|
2008-11-27 22:42:03 +03:00
|
|
|
|
2018-06-12 22:29:22 +03:00
|
|
|
justdatespanp :: Day -> TextParser m DateSpan
|
2018-06-09 05:35:27 +03:00
|
|
|
justdatespanp rdate = do
|
2018-06-12 22:29:22 +03:00
|
|
|
optional (string' "in" >> skipMany spacenonewline)
|
2019-02-14 16:14:52 +03:00
|
|
|
spanFromSmartDate rdate <$> smartdate
|
2008-12-04 22:29:29 +03:00
|
|
|
|
2011-07-17 19:54:58 +04:00
|
|
|
-- | Make a datespan from two valid date strings parseable by parsedate
|
2011-07-18 03:14:51 +04:00
|
|
|
-- (or raise an error). Eg: mkdatespan \"2011/1/1\" \"2011/12/31\".
|
2010-03-09 04:43:25 +03:00
|
|
|
mkdatespan :: String -> String -> DateSpan
|
|
|
|
mkdatespan b = DateSpan (Just $ parsedate b) . Just . parsedate
|
|
|
|
|
2011-06-14 23:10:16 +04:00
|
|
|
nulldatespan :: DateSpan
|
2008-12-04 22:29:29 +03:00
|
|
|
nulldatespan = DateSpan Nothing Nothing
|
|
|
|
|
2018-10-17 23:10:49 +03:00
|
|
|
-- | A datespan of zero length, that matches no date.
|
|
|
|
emptydatespan :: DateSpan
|
|
|
|
emptydatespan = DateSpan (Just $ addDays 1 nulldate) (Just nulldate)
|
|
|
|
|
2011-06-14 23:10:16 +04:00
|
|
|
nulldate :: Day
|
2016-05-27 18:06:53 +03:00
|
|
|
nulldate = fromGregorian 0 1 1
|