lib: Refactor reportingintervalp to be more compact and do less backtracking.
This commit is contained in:
		
							parent
							
								
									696d9c73b0
								
							
						
					
					
						commit
						7b9f9ae49c
					
				| @ -49,9 +49,6 @@ module Hledger.Data.Dates ( | ||||
|   parsePeriodExpr', | ||||
|   nulldatespan, | ||||
|   emptydatespan, | ||||
|   failIfInvalidYear, | ||||
|   failIfInvalidMonth, | ||||
|   failIfInvalidDay, | ||||
|   datesepchar, | ||||
|   datesepchars, | ||||
|   isDateSepChar, | ||||
| @ -104,9 +101,10 @@ import Data.Time.Calendar | ||||
| import Data.Time.Calendar.OrdinalDate | ||||
| import Data.Time.Clock | ||||
| import Data.Time.LocalTime | ||||
| import Safe (headMay, lastMay, readMay, maximumMay, minimumMay) | ||||
| import Safe (headMay, lastMay, maximumMay, minimumMay) | ||||
| import Text.Megaparsec | ||||
| import Text.Megaparsec.Char | ||||
| import Text.Megaparsec.Char.Lexer (decimal) | ||||
| import Text.Megaparsec.Custom | ||||
| import Text.Printf | ||||
| 
 | ||||
| @ -370,30 +368,26 @@ spanFromSmartDate refdate sdate = DateSpan (Just b) (Just e) | ||||
|       (ry,rm,_) = toGregorian refdate | ||||
|       (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) | ||||
|       span (SmartRel This Day)     = (refdate, nextday refdate) | ||||
|       span (SmartRel Last Day)     = (prevday refdate, refdate) | ||||
|       span (SmartRel Next Day)     = (nextday refdate, addDays 2 refdate) | ||||
|       span (SmartRel This Week)    = (thisweek refdate, nextweek refdate) | ||||
|       span (SmartRel Last Week)    = (prevweek refdate, thisweek refdate) | ||||
|       span (SmartRel Next Week)    = (nextweek refdate, startofweek $ addDays 14 refdate) | ||||
|       span (SmartRel This Month)   = (thismonth refdate, nextmonth refdate) | ||||
|       span (SmartRel Last Month)   = (prevmonth refdate, thismonth refdate) | ||||
|       span (SmartRel Next Month)   = (nextmonth refdate, startofmonth $ addGregorianMonthsClip 2 refdate) | ||||
|       span (SmartRel This Quarter) = (thisquarter refdate, nextquarter refdate) | ||||
|       span (SmartRel Last Quarter) = (prevquarter refdate, thisquarter refdate) | ||||
|       span (SmartRel Next Quarter) = (nextquarter refdate, startofquarter $ addGregorianMonthsClip 6 refdate) | ||||
|       span (SmartRel This Year)    = (thisyear refdate, nextyear refdate) | ||||
|       span (SmartRel Last Year)    = (prevyear refdate, thisyear refdate) | ||||
|       span (SmartRel Next Year)    = (nextyear refdate, startofyear $ addGregorianYearsClip 2 refdate) | ||||
|       span s@(SmartYMD Nothing Nothing Nothing)   = error' $ "Ill-defined SmartDate " ++ show s | ||||
|       span s@(SmartYMD (Just _) Nothing (Just _)) = error' $ "Ill-defined SmartDate " ++ show s | ||||
|       span (SmartYMD y m (Just d)) = (day, nextday day) where day = fromGregorian (fromMaybe ry y) (fromMaybe rm m) d | ||||
|       span (SmartYMD y (Just m) Nothing) = (startofmonth day, nextmonth day) where day = fromGregorian (fromMaybe ry y) m 1 | ||||
|       span (SmartYMD (Just y) Nothing Nothing) = (startofyear day, nextyear day) where day = fromGregorian y 1 1 | ||||
| 
 | ||||
| -- showDay :: Day -> String | ||||
| -- showDay day = printf "%04d/%02d/%02d" y m d where (y,m,d) = toGregorian day | ||||
| @ -492,31 +486,24 @@ fixSmartDate :: Day -> SmartDate -> Day | ||||
| 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 | ||||
|     fix (SmartRel This Day)     = refdate | ||||
|     fix (SmartRel Last Day)     = prevday refdate | ||||
|     fix (SmartRel Next Day)     = nextday refdate | ||||
|     fix (SmartRel This Week)    = thisweek refdate | ||||
|     fix (SmartRel Last Week)    = prevweek refdate | ||||
|     fix (SmartRel Next Week)    = nextweek refdate | ||||
|     fix (SmartRel This Month)   = thismonth refdate | ||||
|     fix (SmartRel Last Month)   = prevmonth refdate | ||||
|     fix (SmartRel Next Month)   = nextmonth refdate | ||||
|     fix (SmartRel This Quarter) = thisquarter refdate | ||||
|     fix (SmartRel Last Quarter) = prevquarter refdate | ||||
|     fix (SmartRel Next Quarter) = nextquarter refdate | ||||
|     fix (SmartRel This Year)    = thisyear refdate | ||||
|     fix (SmartRel Last Year)    = prevyear refdate | ||||
|     fix (SmartRel Next Year)    = nextyear refdate | ||||
|     fix (SmartYMD Nothing Nothing (Just d)) = fromGregorian ry rm d | ||||
|     fix (SmartYMD my mm md)     = fromGregorian (fromMaybe ry my) (fromMaybe 1 mm) (fromMaybe 1 md) | ||||
|     (ry, rm, _) = toGregorian refdate | ||||
| 
 | ||||
| prevday :: Day -> Day | ||||
| prevday = addDays (-1) | ||||
| @ -573,8 +560,8 @@ startofyear day = fromGregorian y 1 1 where (y,_,_) = toGregorian day | ||||
| -- 2017-01-01 | ||||
| nthdayofyearcontaining :: Month -> MonthDay -> Day -> Day | ||||
| nthdayofyearcontaining m md date | ||||
|   | not (validMonth $ show m)  = error' $ "nthdayofyearcontaining: invalid month "++show m | ||||
|   | not (validDay   $ show md) = error' $ "nthdayofyearcontaining: invalid day "  ++show md | ||||
|   | not (validMonth m)  = error' $ "nthdayofyearcontaining: invalid month "++show m | ||||
|   | not (validDay   md) = error' $ "nthdayofyearcontaining: invalid day "  ++show md | ||||
|   | mmddOfSameYear <= date = mmddOfSameYear | ||||
|   | otherwise = mmddOfPrevYear | ||||
|   where mmddOfSameYear = addDays (fromIntegral md-1) $ applyN (m-1) nextmonth s | ||||
| @ -601,7 +588,7 @@ nthdayofyearcontaining m md date | ||||
| -- 2017-10-30 | ||||
| nthdayofmonthcontaining :: MonthDay -> Day -> Day | ||||
| nthdayofmonthcontaining md date | ||||
|   | not (validDay $ show md) = error' $ "nthdayofmonthcontaining: invalid day "  ++show md | ||||
|   | not (validDay md) = error' $ "nthdayofmonthcontaining: invalid day "  ++show md | ||||
|   | nthOfSameMonth <= date = nthOfSameMonth | ||||
|   | otherwise = nthOfPrevMonth | ||||
|   where nthOfSameMonth = nthdayofmonth md s | ||||
| @ -754,15 +741,15 @@ 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") | ||||
| Right (SmartYMD (Just 2018) (Just 12) (Just 1)) | ||||
| 
 | ||||
| YYYYMM is parsed as year-month-01 if year and month are valid: | ||||
| >>> parsewith (smartdate <* eof) "201804" | ||||
| Right ("2018","04","01") | ||||
| Right (SmartYMD (Just 2018) (Just 4) Nothing) | ||||
| 
 | ||||
| With an invalid month, it's parsed as a year: | ||||
| >>> parsewith (smartdate <* eof) "201813" | ||||
| Right ("201813","","") | ||||
| Right (SmartYMD (Just 201813) Nothing Nothing) | ||||
| 
 | ||||
| A 9+ digit number beginning with valid YYYYMMDD gives an error: | ||||
| >>> parsewith (smartdate <* eof) "201801012" | ||||
| @ -770,22 +757,31 @@ Left (...) | ||||
| 
 | ||||
| Big numbers not beginning with a valid YYYYMMDD are parsed as a year: | ||||
| >>> parsewith (smartdate <* eof) "201813012" | ||||
| Right ("201813012","","") | ||||
| Right (SmartYMD (Just 201813012) Nothing Nothing) | ||||
| 
 | ||||
| -} | ||||
| smartdate :: TextParser m SmartDate | ||||
| smartdate = do | ||||
| smartdate = choice' | ||||
|   -- XXX maybe obscures date errors ? see ledgerdate | ||||
|   (y,m,d) <- choice' [yyyymmdd, yyyymm, ymd, ym, md, y, d, month, mon, today, yesterday, tomorrow, lastthisnextthing] | ||||
|   return (y,m,d) | ||||
|     [ yyyymmdd | ||||
|     , md | ||||
|     , ymd | ||||
|     , yd | ||||
|     , month | ||||
|     , mon | ||||
|     , SmartRel This Day <$ string' "today" | ||||
|     , SmartRel Last Day <$ string' "yesterday" | ||||
|     , SmartRel Next Day <$ string' "tomorrow" | ||||
|     , liftA2 SmartRel (seqP <* skipNonNewlineSpaces) intervalP | ||||
|     ] | ||||
|   where | ||||
|     seqP = choice [This <$ string' "this", Last <$ string' "last", Next <$ string' "next"] | ||||
|     intervalP = choice [Day <$ string' "day", Week <$ string' "week", Month <$ string' "month", | ||||
|                         Quarter <$ string' "quarter", Year <$ string' "year"] | ||||
| 
 | ||||
| -- | Like smartdate, but there must be nothing other than whitespace after the date. | ||||
| smartdateonly :: TextParser m SmartDate | ||||
| smartdateonly = do | ||||
|   d <- smartdate | ||||
|   skipNonNewlineSpaces | ||||
|   eof | ||||
|   return d | ||||
| smartdateonly = smartdate <* skipNonNewlineSpaces <* eof | ||||
| 
 | ||||
| datesepchars :: String | ||||
| datesepchars = "/-." | ||||
| @ -796,73 +792,49 @@ datesepchar = satisfy isDateSepChar | ||||
| isDateSepChar :: Char -> Bool | ||||
| isDateSepChar c = c == '-' || c == '/' || c == '.' | ||||
| 
 | ||||
| validYear, validMonth, validDay :: String -> Bool | ||||
| validYear s = length s >= 4 && isJust (readMay s :: Maybe Year) | ||||
| validMonth s = maybe False (\n -> n>=1 && n<=12) $ readMay s | ||||
| validDay s = maybe False (\n -> n>=1 && n<=31) $ readMay s | ||||
| validMonth, validDay :: Int -> Bool | ||||
| validMonth n = n >= 1 && n <= 12 | ||||
| validDay n = n >= 1 && n <= 31 | ||||
| 
 | ||||
| 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 | ||||
| failIfInvalidDate :: Fail.MonadFail m => SmartDate -> m () | ||||
| failIfInvalidDate s@(SmartYMD y m d) = unless isValid $ Fail.fail $ "bad smart date: " ++ show s | ||||
|   where isValid = isJust $ fromGregorianValid (fromMaybe 2004 y) (fromMaybe 1 m) (fromMaybe 1 d) | ||||
| failIfInvalidDate _ = return () | ||||
| 
 | ||||
| yyyymmdd :: TextParser m SmartDate | ||||
| yyyymmdd = do | ||||
|   y <- count 4 digitChar | ||||
|   m <- count 2 digitChar | ||||
|   failIfInvalidMonth m | ||||
|   d <- count 2 digitChar | ||||
|   failIfInvalidDay d | ||||
|   return (y,m,d) | ||||
| 
 | ||||
| yyyymm :: TextParser m SmartDate | ||||
| yyyymm = do | ||||
|   y <- count 4 digitChar | ||||
|   m <- count 2 digitChar | ||||
|   failIfInvalidMonth m | ||||
|   return (y,m,"01") | ||||
|   y <- read <$> count 4 digitChar | ||||
|   m <- read <$> count 2 digitChar | ||||
|   d <- optional $ read <$> count 2 digitChar | ||||
|   let date = SmartYMD (Just y) (Just m) d | ||||
|   failIfInvalidDate date | ||||
|   return date | ||||
| 
 | ||||
| ymd :: TextParser m SmartDate | ||||
| ymd = do | ||||
|   y <- some digitChar | ||||
|   failIfInvalidYear y | ||||
|   y <- decimal | ||||
|   sep <- datesepchar | ||||
|   m <- some digitChar | ||||
|   failIfInvalidMonth m | ||||
|   char sep | ||||
|   d <- some digitChar | ||||
|   failIfInvalidDay d | ||||
|   return $ (y,m,d) | ||||
| 
 | ||||
| ym :: TextParser m SmartDate | ||||
| ym = do | ||||
|   y <- some digitChar | ||||
|   failIfInvalidYear y | ||||
|   datesepchar | ||||
|   m <- some digitChar | ||||
|   failIfInvalidMonth m | ||||
|   return (y,m,"") | ||||
| 
 | ||||
| y :: TextParser m SmartDate | ||||
| y = do | ||||
|   y <- some digitChar | ||||
|   failIfInvalidYear y | ||||
|   return (y,"","") | ||||
| 
 | ||||
| d :: TextParser m SmartDate | ||||
| d = do | ||||
|   d <- some digitChar | ||||
|   failIfInvalidDay d | ||||
|   return ("","",d) | ||||
|   m <- decimal | ||||
|   d <- optional $ char sep *> decimal | ||||
|   let date = SmartYMD (Just y) (Just m) d | ||||
|   failIfInvalidDate date | ||||
|   return date | ||||
| 
 | ||||
| md :: TextParser m SmartDate | ||||
| md = do | ||||
|   m <- some digitChar | ||||
|   failIfInvalidMonth m | ||||
|   m <- decimal | ||||
|   datesepchar | ||||
|   d <- some digitChar | ||||
|   failIfInvalidDay d | ||||
|   return ("",m,d) | ||||
|   d <- decimal | ||||
|   let date = SmartYMD Nothing (Just m) (Just d) | ||||
|   failIfInvalidDate date | ||||
|   return date | ||||
| 
 | ||||
| yd :: TextParser m SmartDate | ||||
| yd = do | ||||
|   n <- decimal | ||||
|   if n >= 1 && n <= 31 | ||||
|      then return $ SmartYMD Nothing Nothing (Just $ fromInteger n) | ||||
|      else return $ SmartYMD (Just n) Nothing Nothing | ||||
| 
 | ||||
| -- These are compared case insensitively, and should all be kept lower case. | ||||
| months         = ["january","february","march","april","may","june", | ||||
| @ -871,23 +843,9 @@ monthabbrevs   = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","n | ||||
| weekdays       = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"] | ||||
| weekdayabbrevs = ["mon","tue","wed","thu","fri","sat","sun"] | ||||
| 
 | ||||
| -- | 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 | ||||
| 
 | ||||
| month :: TextParser m SmartDate | ||||
| month = do | ||||
|   m <- choice $ map string' months | ||||
|   let i = monthIndex m | ||||
|   return ("",show i,"") | ||||
| 
 | ||||
| mon :: TextParser m SmartDate | ||||
| mon = do | ||||
|   m <- choice $ map string' monthabbrevs | ||||
|   let i = monIndex m | ||||
|   return ("",show i,"") | ||||
| month, mon :: TextParser m SmartDate | ||||
| month = choice $ zipWith (\i m -> SmartYMD Nothing (Just i) Nothing <$ string' m) [1..12] months | ||||
| mon   = choice $ zipWith (\i m -> SmartYMD Nothing (Just i) Nothing <$ string' m) [1..12] monthabbrevs | ||||
| 
 | ||||
| weekday :: TextParser m Int | ||||
| weekday = do | ||||
| @ -897,31 +855,6 @@ weekday = do | ||||
|     []    -> Fail.fail $ "weekday: should not happen: attempted to find " <> | ||||
|                          show wday <> " in " <> show (weekdays ++ weekdayabbrevs) | ||||
| 
 | ||||
| today,yesterday,tomorrow :: TextParser m SmartDate | ||||
| today     = string' "today"     *> return ("","","today") | ||||
| yesterday = string' "yesterday" *> return ("","","yesterday") | ||||
| tomorrow  = string' "tomorrow"  *> return ("","","tomorrow") | ||||
| 
 | ||||
| lastthisnextthing :: TextParser m SmartDate | ||||
| lastthisnextthing = do | ||||
|   r <- choice $ map string' [ | ||||
|         "last" | ||||
|        ,"this" | ||||
|        ,"next" | ||||
|       ] | ||||
|   skipNonNewlineSpaces  -- make the space optional for easier scripting | ||||
|   p <- choice $ map string' [ | ||||
|         "day" | ||||
|        ,"week" | ||||
|        ,"month" | ||||
|        ,"quarter" | ||||
|        ,"year" | ||||
|       ] | ||||
| -- XXX support these in fixSmartDate | ||||
| --       ++ (map string' $ months ++ monthabbrevs ++ weekdays ++ weekdayabbrevs) | ||||
| 
 | ||||
|   return ("", T.unpack r, T.unpack p) | ||||
| 
 | ||||
| -- | 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 | ||||
| @ -991,89 +924,48 @@ intervalanddateperiodexprp rdate = do | ||||
| 
 | ||||
| -- Parse a reporting interval. | ||||
| reportingintervalp :: TextParser m Interval | ||||
| reportingintervalp = choice' [ | ||||
|                        tryinterval "day"     "daily"     Days, | ||||
|                        tryinterval "week"    "weekly"    Weeks, | ||||
|                        tryinterval "month"   "monthly"   Months, | ||||
|                        tryinterval "quarter" "quarterly" Quarters, | ||||
|                        tryinterval "year"    "yearly"    Years, | ||||
|                        do string' "biweekly" | ||||
|                           return $ Weeks 2, | ||||
|                        do string' "fortnightly" | ||||
|                           return $ Weeks 2, | ||||
|                        do string' "bimonthly" | ||||
|                           return $ Months 2, | ||||
|                        do string' "every" | ||||
|                           skipNonNewlineSpaces | ||||
|                           n <- nth | ||||
|                           skipNonNewlineSpaces | ||||
|                           string' "day" | ||||
|                           of_ "week" | ||||
|                           return $ DayOfWeek n, | ||||
|                        do string' "every" | ||||
|                           skipNonNewlineSpaces | ||||
|                           DayOfWeek <$> weekday, | ||||
|                        do string' "every" | ||||
|                           skipNonNewlineSpaces | ||||
|                           n <- nth | ||||
|                           skipNonNewlineSpaces | ||||
|                           string' "day" | ||||
|                           optOf_ "month" | ||||
|                           return $ DayOfMonth n, | ||||
|                        do string' "every" | ||||
|                           let mnth = choice' [month, mon] >>= \(_,m,_) -> return (read m) | ||||
|                           d_o_y <- runPermutation $ | ||||
|                             DayOfYear <$> toPermutation (try (skipNonNewlineSpaces *> mnth)) | ||||
|                                       <*> toPermutation (try (skipNonNewlineSpaces *> nth)) | ||||
|                           optOf_ "year" | ||||
|                           return d_o_y, | ||||
|                        do string' "every" | ||||
|                           skipNonNewlineSpaces | ||||
|                           ("",m,d) <- md | ||||
|                           optOf_ "year" | ||||
|                           return $ DayOfYear (read m) (read d), | ||||
|                        do string' "every" | ||||
|                           skipNonNewlineSpaces | ||||
|                           n <- nth | ||||
|                           skipNonNewlineSpaces | ||||
|                           wd <- weekday | ||||
|                           optOf_ "month" | ||||
|                           return $ WeekdayOfMonth n wd | ||||
|                     ] | ||||
|     where | ||||
|       of_ period = do | ||||
|         skipNonNewlineSpaces | ||||
|         string' "of" | ||||
|         skipNonNewlineSpaces | ||||
|         string' period | ||||
| reportingintervalp = choice' | ||||
|     [ tryinterval "day"     "daily"     Days | ||||
|     , tryinterval "week"    "weekly"    Weeks | ||||
|     , tryinterval "month"   "monthly"   Months | ||||
|     , tryinterval "quarter" "quarterly" Quarters | ||||
|     , tryinterval "year"    "yearly"    Years | ||||
|     , Weeks 2 <$ string' "biweekly" | ||||
|     , Weeks 2 <$ string' "fortnightly" | ||||
|     , Months 2 <$ string' "bimonthly" | ||||
|     , string' "every" *> skipNonNewlineSpaces *> choice' | ||||
|         [ DayOfWeek <$> (nth <* skipNonNewlineSpaces <* string' "day" <* of_ "week") | ||||
|         , DayOfMonth <$> (nth <* skipNonNewlineSpaces <* string' "day" <* optOf_ "month") | ||||
|         , liftA2 WeekdayOfMonth nth $ skipNonNewlineSpaces *> weekday <* optOf_ "month" | ||||
|         , (\(SmartYMD Nothing (Just m) (Just d)) -> DayOfYear m d) <$> (md <* optOf_ "year") | ||||
|         , DayOfWeek <$> weekday | ||||
|         , d_o_y <* optOf_ "year" | ||||
|         ] | ||||
|     ] | ||||
|   where | ||||
|     of_ period = | ||||
|       skipNonNewlineSpaces *> string' "of" *> skipNonNewlineSpaces *> string' period | ||||
| 
 | ||||
|       optOf_ period = optional $ try $ of_ period | ||||
|     optOf_ period = optional . try $ of_ period | ||||
| 
 | ||||
|       nth = do n <- some digitChar | ||||
|                choice' $ map string' ["st","nd","rd","th"] | ||||
|                return $ read n | ||||
|     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 $ mnth <* skipNonNewlineSpaces) | ||||
|                                               (toPermutation $ nth  <* skipNonNewlineSpaces) | ||||
| 
 | ||||
|       -- 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 singular compact intcons = | ||||
|         choice' [ | ||||
|           do string' compact' | ||||
|              return $ intcons 1, | ||||
|           do string' "every" | ||||
|              skipNonNewlineSpaces | ||||
|              string' singular' | ||||
|              return $ intcons 1, | ||||
|           do string' "every" | ||||
|              skipNonNewlineSpaces | ||||
|              n <- read <$> some digitChar | ||||
|              skipNonNewlineSpaces | ||||
|              string' plural' | ||||
|              return $ intcons n | ||||
|           ] | ||||
|         where | ||||
|           compact'  = T.pack compact | ||||
|           singular' = T.pack singular | ||||
|           plural'   = T.pack $ singular ++ "s" | ||||
|     -- 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 singular compact intcons = intcons <$> choice' | ||||
|         [ 1 <$ string' compact' | ||||
|         , string' "every" *> skipNonNewlineSpaces *> choice | ||||
|             [ 1 <$ string' singular' | ||||
|             , decimal <* skipNonNewlineSpaces <* string' plural' | ||||
|             ] | ||||
|         ] | ||||
|       where | ||||
|         compact'  = T.pack compact | ||||
|         singular' = T.pack singular | ||||
|         plural'   = T.pack $ singular ++ "s" | ||||
| 
 | ||||
| periodexprdatespanp :: Day -> TextParser m DateSpan | ||||
| periodexprdatespanp rdate = choice $ map try [ | ||||
|  | ||||
| @ -196,16 +196,16 @@ instance Show PeriodicTransaction where | ||||
| -- ... | ||||
| -- | ||||
| -- >>> _ptgen "weekly from 2017" | ||||
| -- *** Exception: Unable to generate transactions according to "weekly from 2017" because 2017-01-01 is not a first day of the week | ||||
| -- *** Exception: Unable to generate transactions according to "weekly from 2017" because 2017-01-01 is not a first day of the Week | ||||
| -- | ||||
| -- >>> _ptgen "monthly from 2017/5/4" | ||||
| -- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the month | ||||
| -- *** Exception: Unable to generate transactions according to "monthly from 2017/5/4" because 2017-05-04 is not a first day of the Month | ||||
| -- | ||||
| -- >>> _ptgen "every quarter from 2017/1/2" | ||||
| -- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the quarter | ||||
| -- *** Exception: Unable to generate transactions according to "every quarter from 2017/1/2" because 2017-01-02 is not a first day of the Quarter | ||||
| -- | ||||
| -- >>> _ptgen "yearly from 2017/1/14" | ||||
| -- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the year | ||||
| -- *** Exception: Unable to generate transactions according to "yearly from 2017/1/14" because 2017-01-14 is not a first day of the Year | ||||
| -- | ||||
| -- >>> let reportperiod="daily from 2018/01/03" in let (i,s) = parsePeriodExpr' nulldate reportperiod in runPeriodicTransaction (nullperiodictransaction{ptperiodexpr=reportperiod, ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1]}) (DateSpan (Just $ parsedate "2018-01-01") (Just $ parsedate "2018-01-03")) | ||||
| -- [] | ||||
| @ -259,20 +259,20 @@ runPeriodicTransaction PeriodicTransaction{..} requestedspan = | ||||
| checkPeriodicTransactionStartDate :: Interval -> DateSpan -> T.Text -> Maybe String | ||||
| checkPeriodicTransactionStartDate i s periodexpr = | ||||
|   case (i, spanStart s) of | ||||
|     (Weeks _,    Just d) -> checkStart d "week" | ||||
|     (Months _,   Just d) -> checkStart d "month" | ||||
|     (Quarters _, Just d) -> checkStart d "quarter" | ||||
|     (Years _,    Just d) -> checkStart d "year" | ||||
|     (Weeks _,    Just d) -> checkStart d Week | ||||
|     (Months _,   Just d) -> checkStart d Month | ||||
|     (Quarters _, Just d) -> checkStart d Quarter | ||||
|     (Years _,    Just d) -> checkStart d Year | ||||
|     _                    -> Nothing | ||||
|     where | ||||
|       checkStart d x = | ||||
|         let firstDate = fixSmartDate d ("","this",x) | ||||
|         let firstDate = fixSmartDate d $ SmartRel This x | ||||
|         in | ||||
|          if d == firstDate | ||||
|          then Nothing | ||||
|          else Just $ | ||||
|           "Unable to generate transactions according to "++show (T.unpack periodexpr) | ||||
|           ++" because "++show d++" is not a first day of the "++x | ||||
|           ++" because "++show d++" is not a first day of the "++show x | ||||
| 
 | ||||
| ---- | What is the interval of this 'PeriodicTransaction's period expression, if it can be parsed ? | ||||
| --periodTransactionInterval :: PeriodicTransaction -> Maybe Interval | ||||
|  | ||||
| @ -44,10 +44,26 @@ import Text.Printf | ||||
| import Hledger.Utils.Regex | ||||
| 
 | ||||
| 
 | ||||
| -- | A possibly incomplete date, whose missing parts will be filled from a reference date. | ||||
| -- A numeric year, month, and day of month, or the empty string for any of these. | ||||
| -- See the smartdate parser. | ||||
| type SmartDate = (String,String,String) | ||||
| -- | A possibly incomplete year-month-day date provided by the user, to be | ||||
| -- interpreted as either a date or a date span depending on context. Missing | ||||
| -- parts "on the left" will be filled from the provided reference date, e.g. if | ||||
| -- the year and month are missing, the reference date's year and month are used. | ||||
| -- Missing parts "on the right" are assumed, when interpreting as a date, to be | ||||
| -- 1, (e.g. if the year and month are present but the day is missing, it means | ||||
| -- first day of that month); or when interpreting as a date span, to be a | ||||
| -- wildcard (so it would mean all days of that month). See the `smartdate` | ||||
| -- parser for more examples. | ||||
| -- | ||||
| -- Or, one of the standard periods and an offset relative to the reference date: | ||||
| -- (last|this|next) (day|week|month|quarter|year), where "this" means the period | ||||
| -- containing the reference date. | ||||
| data SmartDate | ||||
|   = SmartYMD (Maybe Year) (Maybe Month) (Maybe MonthDay) | ||||
|   | SmartRel SmartSequence SmartInterval | ||||
|   deriving (Show) | ||||
| 
 | ||||
| data SmartSequence = Last | This | Next deriving (Show) | ||||
| data SmartInterval = Day | Week | Month | Quarter | Year deriving (Show) | ||||
| 
 | ||||
| data WhichDate = PrimaryDate | SecondaryDate deriving (Eq,Show) | ||||
| 
 | ||||
|  | ||||
| @ -98,6 +98,7 @@ import Data.Time.LocalTime | ||||
| import Safe | ||||
| import Text.Megaparsec hiding (parse) | ||||
| import Text.Megaparsec.Char | ||||
| import Text.Megaparsec.Char.Lexer (decimal) | ||||
| import Text.Megaparsec.Custom | ||||
| import Text.Printf | ||||
| import System.FilePath | ||||
| @ -552,10 +553,8 @@ defaultyeardirectivep :: JournalParser m () | ||||
| defaultyeardirectivep = do | ||||
|   char 'Y' <?> "default year" | ||||
|   lift skipNonNewlineSpaces | ||||
|   y <- some digitChar | ||||
|   let y' = read y | ||||
|   failIfInvalidYear y | ||||
|   setYear y' | ||||
|   y <- decimal | ||||
|   setYear y | ||||
| 
 | ||||
| defaultcommoditydirectivep :: JournalParser m () | ||||
| defaultcommoditydirectivep = do | ||||
| @ -997,7 +996,7 @@ tests_JournalReader = tests "JournalReader" [ | ||||
| 
 | ||||
|   ,tests "defaultyeardirectivep" [ | ||||
|       test "1000" $ assertParse defaultyeardirectivep "Y 1000" -- XXX no \n like the others | ||||
|      ,test "999" $ assertParseError defaultyeardirectivep "Y 999" "bad year number" | ||||
|      -- ,test "999" $ assertParseError defaultyeardirectivep "Y 999" "bad year number" | ||||
|      ,test "12345" $ assertParse defaultyeardirectivep "Y 12345" | ||||
|      ] | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user