darcs-2.18.5: src/Darcs/Util/DateMatcher.hs
-- Copyright (C) 2004 David Roundy
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2, or (at your option)
-- any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; see the file COPYING. If not, write to
-- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
-- Boston, MA 02110-1301, USA.
-- |
-- Module : Darcs.Util.DateMatcher
-- Copyright : 2004 David Roundy
-- License : GPL
-- Maintainer : darcs-devel@darcs.net
-- Stability : experimental
-- Portability : portable
module Darcs.Util.DateMatcher
(
parseDateMatcher
-- for debugging only
, DateMatcher(..)
, getMatchers
-- for testing (GHCi, etc)
, testDate
, testDateAt
) where
import Darcs.Prelude
import Control.Exception ( catchJust )
import Data.Maybe ( isJust )
import System.IO.Error ( isUserError, ioeGetErrorString )
import System.Time
import Text.ParserCombinators.Parsec ( eof, parse, ParseError )
import Darcs.Util.IsoDate
( parseDate, englishDateTime, englishInterval, englishLast
, iso8601Interval, resetCalendar, subtractFromMCal, getLocalTz
, MCalendarTime(..), toMCalendarTime, unsafeToCalendarTime
, unsetTime, readUTCDate
)
-- | 'withinDay' @x y@ is true if @x <= y < (x + one_day)@
-- Note that this converts the two dates to @ClockTime@ to avoid
-- any timezone-related errors
withinDay :: CalendarTime -> CalendarTime -> Bool
withinDay a b = within (Just $ toClockTime a)
(Just (addToClockTime day $ toClockTime a))
(toClockTime b)
where
day = TimeDiff 0 0 1 0 0 0 0
-- | 'dateRange' @x1 x2 y@ is true if @x1 <= y < x2@
-- Since @x1@ and @x2@ can be underspecified, we simply assume the
-- first date that they could stand for.
dateRange :: Maybe MCalendarTime -> Maybe MCalendarTime -> CalendarTime -> Bool
dateRange a b = cDateRange (fmap unsafeToCalendarTime a)
(fmap unsafeToCalendarTime b)
-- | 'cDateRange' @x1 x2 y@ is true if @x1 <= y < x2@
cDateRange :: Maybe CalendarTime -> Maybe CalendarTime -> CalendarTime -> Bool
cDateRange a b c = within (fmap toClockTime a)
(fmap toClockTime b) (toClockTime c)
-- | 'within' @x1 x2 y@ is true if @x1 <= y < x2@
within :: Maybe ClockTime -> Maybe ClockTime -> ClockTime -> Bool
within (Just start) (Just end) time = start <= time && time < end
within Nothing (Just end) time = time < end
within (Just start) Nothing time = start <= time
within _ _ _ = undefined
-- | 'samePartialDate' @range exact@ is true if @exact@ falls
-- within the a range of dates represented by @range@.
-- The purpose of this function is to support matching on partially
-- specified dates. That is, if you only specify the date 2007,
-- this function should match any dates within that year. On the
-- other hand, if you specify 2007-01, this function will match any
-- dates within that month. This function only matches up to the
-- second.
samePartialDate :: MCalendarTime -> CalendarTime -> Bool
samePartialDate a b_ =
within (Just clockA)
(Just $ addToClockTime interval clockA)
(toClockTime calB)
where
interval
| isJust (mctSec a) = second
| isJust (mctMin a) = minute
| isJust (mctHour a) = hour
| isJust (mctYDay a) = day
| mctWeek a = maybe week (const day) (mctWDay a)
| isJust (mctDay a) = day
| isJust (mctMonth a) = month
| otherwise = year
year = TimeDiff 1 0 0 0 0 0 0
month = TimeDiff 0 1 0 0 0 0 0
week = TimeDiff 0 0 7 0 0 0 0
day = TimeDiff 0 0 1 0 0 0 0
hour = TimeDiff 0 0 0 1 0 0 0
minute = TimeDiff 0 0 0 0 1 0 0
second = TimeDiff 0 0 0 0 0 1 0
clockA = toClockTime $ unsafeToCalendarTime a
calB = resetCalendar b_
-- | A 'DateMatcher' combines a potential parse for a date string
-- with a "matcher" function that operates on a given date.
-- We use an existential type on the matcher to allow
-- the date string to either be interpreted as a point in time
-- or as an interval.
data DateMatcher = forall d . (Show d) => DM
String -- name
(Either ParseError d) -- parser
(d -> CalendarTime -> Bool) -- matcher
-- | 'parseDateMatcher' @s@ return the first matcher in
-- 'getMatchers' that can parse 's'
parseDateMatcher :: String -> IO (CalendarTime -> Bool)
parseDateMatcher d = testDateMatcher `catchUserError` handleError
where
catchUserError = catchJust $ \e ->
if isUserError e then Just (ioeGetErrorString e) else Nothing
-- If the user enters a date > maxint seconds ago, the toClockTime
-- function cannot work.
handleError e = if e == "Time.toClockTime: invalid input"
then error "Can't handle dates that far back!"
else error e
-- Hack: test the matcher against the current date and discard the results.
-- We just want to make sure it won't throw any exceptions when we use it
-- for real.
testDateMatcher = do
matcher <- tryMatchers `fmap` getMatchers d
matcher `fmap` now >>= (`seq` return matcher)
-- | 'getMatchers' @d@ returns the list of matchers that will be
-- applied on @d@. If you wish to extend the date parsing code,
-- this will likely be the function that you modify to do so.
getMatchers :: String -> IO [DateMatcher]
getMatchers d = do
rightNow <- now
let midnightToday = unsetTime rightNow
mRightNow = toMCalendarTime rightNow
matchIsoInterval (Left dur) =
let durAgo = dur `subtractFromMCal` mRightNow in
dateRange (Just durAgo) (Just mRightNow)
matchIsoInterval (Right (a,b)) = dateRange (Just a) (Just b)
tzNow <- getLocalTz
return
-- note that the order of these is quite important as some matchers can
-- match the same date.
[ DM "from English date"
(parseDateWith $ englishLast midnightToday)
(\(a,_) -> cDateRange (Just a) Nothing)
, DM "specific English date"
(parseDateWith $ englishDateTime midnightToday)
withinDay
, DM "English interval"
(parseDateWith $ englishInterval rightNow)
(uncurry cDateRange)
, DM "ISO 8601 interval"
(parseDateWith $ iso8601Interval tzNow)
matchIsoInterval
, DM "CVS, ISO 8601, old style, or RFC2822 date"
(parseDate tzNow d)
samePartialDate
]
where
tillEof p = do { x <- p; eof; return x }
parseDateWith p = parse (tillEof p) "" d
--- The following functions are for toying around in GHCi
---
--- > testDate "2008/05/22 10:34"
--- > testDateAt "2006-03-22 09:36" "2008/05/22 10:34"
-- | 'tryMatchers' @ms@ returns the first successful match in @ms@
-- It is an error if there are no matches
tryMatchers :: [DateMatcher] -> CalendarTime -> Bool
tryMatchers (DM _ parsed matcher : ms) =
case parsed of
Left _ -> tryMatchers ms
Right d -> matcher d
tryMatchers [] = error "Can't support fancy dates."
now :: IO CalendarTime
now = getClockTime >>= toCalendarTime
-- | 'testDate' @d@ shows the possible interpretations
-- for the date string @d@ and how they match against
-- the current date
testDate :: String -> IO ()
testDate d = do cnow <- now
testDateAtCal cnow d
-- | 'testDate' @iso d@ shows the possible interpretations
-- for the date string @d@ and how they match against
-- the date represented by the ISO 8601 string @iso@
testDateAt :: String -> String -> IO ()
testDateAt iso = testDateAtCal (readUTCDate iso)
-- | helper function for 'testDate' and 'testDateAt'
testDateAtCal :: CalendarTime -> String -> IO ()
testDateAtCal c d =
do ms <- getMatchers d
putStr . unlines . map (showMatcher c) $ ms
-- | 'showMatcher' @c dm@ tells us if @dm@ applies to
-- 'CalendarTime' @c@; or if @dm@ just represents the
-- failure to parse a date, in which case @c@ is moot.
showMatcher :: CalendarTime -> DateMatcher -> String
showMatcher cnow (DM n p m) =
"==== " ++ n ++ " ====\n" ++
(case p of
Left err -> shows err ""
Right x -> show x ++ "\n" ++ show (m x cnow))