lib: Make ill-formed SmartDates unrepresentable.
This commit is contained in:
parent
27d6b21dff
commit
f2dcbd2fee
@ -370,27 +370,26 @@ spanFromSmartDate refdate sdate = DateSpan (Just b) (Just e)
|
|||||||
(ry,rm,_) = toGregorian refdate
|
(ry,rm,_) = toGregorian refdate
|
||||||
(b,e) = span sdate
|
(b,e) = span sdate
|
||||||
span :: SmartDate -> (Day,Day)
|
span :: SmartDate -> (Day,Day)
|
||||||
span (SmartRel This Day) = (refdate, nextday refdate)
|
span (SmartRelative This Day) = (refdate, nextday refdate)
|
||||||
span (SmartRel Last Day) = (prevday refdate, refdate)
|
span (SmartRelative Last Day) = (prevday refdate, refdate)
|
||||||
span (SmartRel Next Day) = (nextday refdate, addDays 2 refdate)
|
span (SmartRelative Next Day) = (nextday refdate, addDays 2 refdate)
|
||||||
span (SmartRel This Week) = (thisweek refdate, nextweek refdate)
|
span (SmartRelative This Week) = (thisweek refdate, nextweek refdate)
|
||||||
span (SmartRel Last Week) = (prevweek refdate, thisweek refdate)
|
span (SmartRelative Last Week) = (prevweek refdate, thisweek refdate)
|
||||||
span (SmartRel Next Week) = (nextweek refdate, startofweek $ addDays 14 refdate)
|
span (SmartRelative Next Week) = (nextweek refdate, startofweek $ addDays 14 refdate)
|
||||||
span (SmartRel This Month) = (thismonth refdate, nextmonth refdate)
|
span (SmartRelative This Month) = (thismonth refdate, nextmonth refdate)
|
||||||
span (SmartRel Last Month) = (prevmonth refdate, thismonth refdate)
|
span (SmartRelative Last Month) = (prevmonth refdate, thismonth refdate)
|
||||||
span (SmartRel Next Month) = (nextmonth refdate, startofmonth $ addGregorianMonthsClip 2 refdate)
|
span (SmartRelative Next Month) = (nextmonth refdate, startofmonth $ addGregorianMonthsClip 2 refdate)
|
||||||
span (SmartRel This Quarter) = (thisquarter refdate, nextquarter refdate)
|
span (SmartRelative This Quarter) = (thisquarter refdate, nextquarter refdate)
|
||||||
span (SmartRel Last Quarter) = (prevquarter refdate, thisquarter refdate)
|
span (SmartRelative Last Quarter) = (prevquarter refdate, thisquarter refdate)
|
||||||
span (SmartRel Next Quarter) = (nextquarter refdate, startofquarter $ addGregorianMonthsClip 6 refdate)
|
span (SmartRelative Next Quarter) = (nextquarter refdate, startofquarter $ addGregorianMonthsClip 6 refdate)
|
||||||
span (SmartRel This Year) = (thisyear refdate, nextyear refdate)
|
span (SmartRelative This Year) = (thisyear refdate, nextyear refdate)
|
||||||
span (SmartRel Last Year) = (prevyear refdate, thisyear refdate)
|
span (SmartRelative Last Year) = (prevyear refdate, thisyear refdate)
|
||||||
span (SmartRel Next Year) = (nextyear refdate, startofyear $ addGregorianYearsClip 2 refdate)
|
span (SmartRelative Next Year) = (nextyear refdate, startofyear $ addGregorianYearsClip 2 refdate)
|
||||||
-- PARTIAL:
|
span (SmartAssumeStart y Nothing) = (startofyear day, nextyear day) where day = fromGregorian y 1 1
|
||||||
span s@(SmartYMD Nothing Nothing Nothing) = error' $ "Ill-defined SmartDate " ++ show s
|
span (SmartAssumeStart y (Just (m, Nothing))) = (startofmonth day, nextmonth day) where day = fromGregorian y m 1
|
||||||
span s@(SmartYMD (Just _) Nothing (Just _)) = error' $ "Ill-defined SmartDate " ++ show s
|
span (SmartAssumeStart y (Just (m, Just d))) = (day, nextday day) where day = fromGregorian y m d
|
||||||
span (SmartYMD y m (Just d)) = (day, nextday day) where day = fromGregorian (fromMaybe ry y) (fromMaybe rm m) d
|
span (SmartFromReference m d) = (day, nextday day) where day = fromGregorian ry (fromMaybe rm m) d
|
||||||
span (SmartYMD y (Just m) Nothing) = (startofmonth day, nextmonth day) where day = fromGregorian (fromMaybe ry y) m 1
|
span (SmartMonth m) = (startofmonth day, nextmonth day) where day = fromGregorian ry m 1
|
||||||
span (SmartYMD (Just y) Nothing Nothing) = (startofyear day, nextyear day) where day = fromGregorian y 1 1
|
|
||||||
|
|
||||||
-- showDay :: Day -> String
|
-- showDay :: Day -> String
|
||||||
-- showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day
|
-- showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day
|
||||||
@ -489,23 +488,24 @@ fixSmartDate :: Day -> SmartDate -> Day
|
|||||||
fixSmartDate refdate = fix
|
fixSmartDate refdate = fix
|
||||||
where
|
where
|
||||||
fix :: SmartDate -> Day
|
fix :: SmartDate -> Day
|
||||||
fix (SmartRel This Day) = refdate
|
fix (SmartRelative This Day) = refdate
|
||||||
fix (SmartRel Last Day) = prevday refdate
|
fix (SmartRelative Last Day) = prevday refdate
|
||||||
fix (SmartRel Next Day) = nextday refdate
|
fix (SmartRelative Next Day) = nextday refdate
|
||||||
fix (SmartRel This Week) = thisweek refdate
|
fix (SmartRelative This Week) = thisweek refdate
|
||||||
fix (SmartRel Last Week) = prevweek refdate
|
fix (SmartRelative Last Week) = prevweek refdate
|
||||||
fix (SmartRel Next Week) = nextweek refdate
|
fix (SmartRelative Next Week) = nextweek refdate
|
||||||
fix (SmartRel This Month) = thismonth refdate
|
fix (SmartRelative This Month) = thismonth refdate
|
||||||
fix (SmartRel Last Month) = prevmonth refdate
|
fix (SmartRelative Last Month) = prevmonth refdate
|
||||||
fix (SmartRel Next Month) = nextmonth refdate
|
fix (SmartRelative Next Month) = nextmonth refdate
|
||||||
fix (SmartRel This Quarter) = thisquarter refdate
|
fix (SmartRelative This Quarter) = thisquarter refdate
|
||||||
fix (SmartRel Last Quarter) = prevquarter refdate
|
fix (SmartRelative Last Quarter) = prevquarter refdate
|
||||||
fix (SmartRel Next Quarter) = nextquarter refdate
|
fix (SmartRelative Next Quarter) = nextquarter refdate
|
||||||
fix (SmartRel This Year) = thisyear refdate
|
fix (SmartRelative This Year) = thisyear refdate
|
||||||
fix (SmartRel Last Year) = prevyear refdate
|
fix (SmartRelative Last Year) = prevyear refdate
|
||||||
fix (SmartRel Next Year) = nextyear refdate
|
fix (SmartRelative Next Year) = nextyear refdate
|
||||||
fix (SmartYMD Nothing Nothing (Just d)) = fromGregorian ry rm d
|
fix (SmartAssumeStart y md) = fromGregorian y (maybe 1 fst md) (fromMaybe 1 $ snd =<< md)
|
||||||
fix (SmartYMD my mm md) = fromGregorian (fromMaybe ry my) (fromMaybe 1 mm) (fromMaybe 1 md)
|
fix (SmartFromReference m d) = fromGregorian ry (fromMaybe rm m) d
|
||||||
|
fix (SmartMonth m) = fromGregorian ry m 1
|
||||||
(ry, rm, _) = toGregorian refdate
|
(ry, rm, _) = toGregorian refdate
|
||||||
|
|
||||||
prevday :: Day -> Day
|
prevday :: Day -> Day
|
||||||
@ -748,15 +748,15 @@ Eg:
|
|||||||
YYYYMMDD is parsed as year-month-date if those parts are valid
|
YYYYMMDD is parsed as year-month-date if those parts are valid
|
||||||
(>=4 digits, 1-12, and 1-31 respectively):
|
(>=4 digits, 1-12, and 1-31 respectively):
|
||||||
>>> parsewith (smartdate <* eof) "20181201"
|
>>> parsewith (smartdate <* eof) "20181201"
|
||||||
Right (SmartYMD (Just 2018) (Just 12) (Just 1))
|
Right (SmartAssumeStart 2018 (Just (12,Just 1)))
|
||||||
|
|
||||||
YYYYMM is parsed as year-month-01 if year and month are valid:
|
YYYYMM is parsed as year-month-01 if year and month are valid:
|
||||||
>>> parsewith (smartdate <* eof) "201804"
|
>>> parsewith (smartdate <* eof) "201804"
|
||||||
Right (SmartYMD (Just 2018) (Just 4) Nothing)
|
Right (SmartAssumeStart 2018 (Just (4,Nothing)))
|
||||||
|
|
||||||
With an invalid month, it's parsed as a year:
|
With an invalid month, it's parsed as a year:
|
||||||
>>> parsewith (smartdate <* eof) "201813"
|
>>> parsewith (smartdate <* eof) "201813"
|
||||||
Right (SmartYMD (Just 201813) Nothing Nothing)
|
Right (SmartAssumeStart 201813 Nothing)
|
||||||
|
|
||||||
A 9+ digit number beginning with valid YYYYMMDD gives an error:
|
A 9+ digit number beginning with valid YYYYMMDD gives an error:
|
||||||
>>> parsewith (smartdate <* eof) "201801012"
|
>>> parsewith (smartdate <* eof) "201801012"
|
||||||
@ -764,27 +764,25 @@ Left (...)
|
|||||||
|
|
||||||
Big numbers not beginning with a valid YYYYMMDD are parsed as a year:
|
Big numbers not beginning with a valid YYYYMMDD are parsed as a year:
|
||||||
>>> parsewith (smartdate <* eof) "201813012"
|
>>> parsewith (smartdate <* eof) "201813012"
|
||||||
Right (SmartYMD (Just 201813012) Nothing Nothing)
|
Right (SmartAssumeStart 201813012 Nothing)
|
||||||
|
|
||||||
-}
|
-}
|
||||||
smartdate :: TextParser m SmartDate
|
smartdate :: TextParser m SmartDate
|
||||||
smartdate = choice'
|
smartdate = choice'
|
||||||
-- XXX maybe obscures date errors ? see ledgerdate
|
-- XXX maybe obscures date errors ? see ledgerdate
|
||||||
[ yyyymmdd, md, ymd, smartYear, smartDay, month, mon
|
[ yyyymmdd, ymd
|
||||||
, SmartRel This Day <$ string' "today"
|
, (\(m,d) -> SmartFromReference (Just m) d) <$> md
|
||||||
, SmartRel Last Day <$ string' "yesterday"
|
, (SmartFromReference Nothing <$> decimal) >>= failIfInvalidDate
|
||||||
, SmartRel Next Day <$ string' "tomorrow"
|
, SmartMonth <$> (month <|> mon)
|
||||||
, liftA2 SmartRel (seqP <* skipNonNewlineSpaces) intervalP
|
, SmartRelative This Day <$ string' "today"
|
||||||
|
, SmartRelative Last Day <$ string' "yesterday"
|
||||||
|
, SmartRelative Next Day <$ string' "tomorrow"
|
||||||
|
, liftA2 SmartRelative (seqP <* skipNonNewlineSpaces) intervalP
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
seqP = choice [This <$ string' "this", Last <$ string' "last", Next <$ string' "next"]
|
seqP = choice [This <$ string' "this", Last <$ string' "last", Next <$ string' "next"]
|
||||||
intervalP = choice [Day <$ string' "day", Week <$ string' "week", Month <$ string' "month",
|
intervalP = choice [Day <$ string' "day", Week <$ string' "week", Month <$ string' "month",
|
||||||
Quarter <$ string' "quarter", Year <$ string' "year"]
|
Quarter <$ string' "quarter", Year <$ string' "year"]
|
||||||
smartYear = (\y -> SmartYMD (Just y) Nothing Nothing) <$> yearp
|
|
||||||
smartDay = do
|
|
||||||
d <- SmartYMD Nothing Nothing . Just <$> decimal
|
|
||||||
failIfInvalidDate d
|
|
||||||
return d
|
|
||||||
|
|
||||||
-- | Like smartdate, but there must be nothing other than whitespace after the date.
|
-- | Like smartdate, but there must be nothing other than whitespace after the date.
|
||||||
smartdateonly :: TextParser m SmartDate
|
smartdateonly :: TextParser m SmartDate
|
||||||
@ -803,38 +801,35 @@ validMonth, validDay :: Int -> Bool
|
|||||||
validMonth n = n >= 1 && n <= 12
|
validMonth n = n >= 1 && n <= 12
|
||||||
validDay n = n >= 1 && n <= 31
|
validDay n = n >= 1 && n <= 31
|
||||||
|
|
||||||
failIfInvalidDate :: Fail.MonadFail m => SmartDate -> m ()
|
failIfInvalidDate :: Fail.MonadFail m => SmartDate -> m SmartDate
|
||||||
failIfInvalidDate s@(SmartYMD y m d) = unless isValid $ Fail.fail $ "bad smart date: " ++ show s
|
failIfInvalidDate s = unless isValid (Fail.fail $ "bad smart date: " ++ show s) *> return s
|
||||||
where isValid = isJust $ fromGregorianValid (fromMaybe 2004 y) (fromMaybe 1 m) (fromMaybe 1 d)
|
where isValid = case s of
|
||||||
failIfInvalidDate _ = return ()
|
SmartAssumeStart y (Just (m, md)) -> isJust $ fromGregorianValid y m (fromMaybe 1 md)
|
||||||
|
SmartFromReference mm d -> isJust $ fromGregorianValid 2004 (fromMaybe 1 mm) d
|
||||||
|
SmartMonth m -> validMonth m
|
||||||
|
_ -> True
|
||||||
|
|
||||||
yyyymmdd :: TextParser m SmartDate
|
yyyymmdd :: TextParser m SmartDate
|
||||||
yyyymmdd = do
|
yyyymmdd = do
|
||||||
y <- read <$> count 4 digitChar
|
y <- read <$> count 4 digitChar
|
||||||
m <- read <$> count 2 digitChar
|
m <- read <$> count 2 digitChar
|
||||||
d <- optional $ read <$> count 2 digitChar
|
d <- optional $ read <$> count 2 digitChar
|
||||||
let date = SmartYMD (Just y) (Just m) d
|
let date = SmartAssumeStart y $ Just (m, d)
|
||||||
failIfInvalidDate date
|
failIfInvalidDate date
|
||||||
return date
|
|
||||||
|
|
||||||
ymd :: TextParser m SmartDate
|
ymd :: TextParser m SmartDate
|
||||||
ymd = do
|
ymd = liftA2 SmartAssumeStart yearp (optional $ try monthday) >>= failIfInvalidDate
|
||||||
y <- yearp
|
where monthday = do
|
||||||
sep <- datesepchar
|
sep <- datesepchar
|
||||||
m <- decimal
|
liftA2 (,) decimal . optional $ char sep *> decimal
|
||||||
d <- optional $ char sep *> decimal
|
|
||||||
let date = SmartYMD (Just y) (Just m) d
|
|
||||||
failIfInvalidDate date
|
|
||||||
return date
|
|
||||||
|
|
||||||
md :: TextParser m SmartDate
|
md :: TextParser m (Month, MonthDay)
|
||||||
md = do
|
md = do
|
||||||
m <- decimal
|
m <- decimal
|
||||||
datesepchar
|
datesepchar
|
||||||
d <- decimal
|
d <- decimal
|
||||||
let date = SmartYMD Nothing (Just m) (Just d)
|
_ <- failIfInvalidDate $ SmartFromReference (Just m) d
|
||||||
failIfInvalidDate date
|
return (m, d)
|
||||||
return date
|
|
||||||
|
|
||||||
-- | Parse a year number from a Text, making sure that at least four digits are
|
-- | Parse a year number from a Text, making sure that at least four digits are
|
||||||
-- used.
|
-- used.
|
||||||
@ -851,9 +846,9 @@ monthabbrevs = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","n
|
|||||||
weekdays = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]
|
weekdays = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]
|
||||||
weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]
|
weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"]
|
||||||
|
|
||||||
month, mon :: TextParser m SmartDate
|
month, mon :: TextParser m Month
|
||||||
month = choice $ zipWith (\i m -> SmartYMD Nothing (Just i) Nothing <$ string' m) [1..12] months
|
month = choice $ zipWith (\i m -> i <$ string' m) [1..12] months
|
||||||
mon = choice $ zipWith (\i m -> SmartYMD Nothing (Just i) Nothing <$ string' m) [1..12] monthabbrevs
|
mon = choice $ zipWith (\i m -> i <$ string' m) [1..12] monthabbrevs
|
||||||
|
|
||||||
weekday :: TextParser m Int
|
weekday :: TextParser m Int
|
||||||
weekday = do
|
weekday = do
|
||||||
@ -920,10 +915,9 @@ weekday = do
|
|||||||
periodexprp :: Day -> TextParser m (Interval, DateSpan)
|
periodexprp :: Day -> TextParser m (Interval, DateSpan)
|
||||||
periodexprp rdate = do
|
periodexprp rdate = do
|
||||||
skipNonNewlineSpaces
|
skipNonNewlineSpaces
|
||||||
choice $ map try [
|
choice' [ intervalanddateperiodexprp rdate
|
||||||
intervalanddateperiodexprp rdate,
|
, (,) NoInterval <$> periodexprdatespanp rdate
|
||||||
(,) NoInterval <$> periodexprdatespanp rdate
|
]
|
||||||
]
|
|
||||||
|
|
||||||
-- Parse a reporting interval and a date span.
|
-- Parse a reporting interval and a date span.
|
||||||
intervalanddateperiodexprp :: Day -> TextParser m (Interval, DateSpan)
|
intervalanddateperiodexprp :: Day -> TextParser m (Interval, DateSpan)
|
||||||
@ -949,7 +943,7 @@ reportingintervalp = choice'
|
|||||||
[ DayOfWeek <$> (nth <* skipNonNewlineSpaces <* string' "day" <* of_ "week")
|
[ DayOfWeek <$> (nth <* skipNonNewlineSpaces <* string' "day" <* of_ "week")
|
||||||
, DayOfMonth <$> (nth <* skipNonNewlineSpaces <* string' "day" <* optOf_ "month")
|
, DayOfMonth <$> (nth <* skipNonNewlineSpaces <* string' "day" <* optOf_ "month")
|
||||||
, liftA2 WeekdayOfMonth nth $ skipNonNewlineSpaces *> weekday <* optOf_ "month"
|
, liftA2 WeekdayOfMonth nth $ skipNonNewlineSpaces *> weekday <* optOf_ "month"
|
||||||
, (\(SmartYMD Nothing (Just m) (Just d)) -> DayOfYear m d) <$> (md <* optOf_ "year")
|
, uncurry DayOfYear <$> (md <* optOf_ "year")
|
||||||
, DayOfWeek <$> weekday
|
, DayOfWeek <$> weekday
|
||||||
, d_o_y <* optOf_ "year"
|
, d_o_y <* optOf_ "year"
|
||||||
]
|
]
|
||||||
@ -961,9 +955,8 @@ reportingintervalp = choice'
|
|||||||
optOf_ period = optional . try $ of_ period
|
optOf_ period = optional . try $ of_ period
|
||||||
|
|
||||||
nth = decimal <* choice (map string' ["st","nd","rd","th"])
|
nth = decimal <* choice (map string' ["st","nd","rd","th"])
|
||||||
mnth = (\(SmartYMD Nothing (Just m) Nothing) -> m) <$> choice' [month, mon]
|
d_o_y = runPermutation $ liftA2 DayOfYear (toPermutation $ (month <|> mon) <* skipNonNewlineSpaces)
|
||||||
d_o_y = runPermutation $ liftA2 DayOfYear (toPermutation $ mnth <* skipNonNewlineSpaces)
|
(toPermutation $ nth <* skipNonNewlineSpaces)
|
||||||
(toPermutation $ nth <* skipNonNewlineSpaces)
|
|
||||||
|
|
||||||
-- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".
|
-- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".
|
||||||
tryinterval :: String -> String -> (Int -> Interval) -> TextParser m Interval
|
tryinterval :: String -> String -> (Int -> Interval) -> TextParser m Interval
|
||||||
|
|||||||
@ -266,7 +266,7 @@ checkPeriodicTransactionStartDate i s periodexpr =
|
|||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
where
|
where
|
||||||
checkStart d x =
|
checkStart d x =
|
||||||
let firstDate = fixSmartDate d $ SmartRel This x
|
let firstDate = fixSmartDate d $ SmartRelative This x
|
||||||
in
|
in
|
||||||
if d == firstDate
|
if d == firstDate
|
||||||
then Nothing
|
then Nothing
|
||||||
|
|||||||
@ -58,8 +58,10 @@ import Hledger.Utils.Regex
|
|||||||
-- (last|this|next) (day|week|month|quarter|year), where "this" means the period
|
-- (last|this|next) (day|week|month|quarter|year), where "this" means the period
|
||||||
-- containing the reference date.
|
-- containing the reference date.
|
||||||
data SmartDate
|
data SmartDate
|
||||||
= SmartYMD (Maybe Year) (Maybe Month) (Maybe MonthDay)
|
= SmartAssumeStart Year (Maybe (Month, Maybe MonthDay))
|
||||||
| SmartRel SmartSequence SmartInterval
|
| SmartFromReference (Maybe Month) MonthDay
|
||||||
|
| SmartMonth Month
|
||||||
|
| SmartRelative SmartSequence SmartInterval
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data SmartSequence = Last | This | Next deriving (Show)
|
data SmartSequence = Last | This | Next deriving (Show)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user