{-# LANGUAGE TemplateHaskell #-}
module Text.Password.Strength.Internal.Date (
Date,
YMD,
isDate,
toYMD,
estimateDate
) where
import Control.Lens ((&), (^.), (+~), _1)
import Control.Lens.TH (makeLenses)
import qualified Data.Attoparsec.Text as Atto
import Data.Char (isDigit)
import Data.Char (isSpace)
import Data.Function (on)
import Data.List (sortBy)
import Data.Maybe (catMaybes, listToMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Read as Text
import qualified Data.Time.Calendar as Time
data Date = Date
{ _year :: Int
, _month :: Int
, _day :: Int
, _hasSep :: Bool
, _refYear :: Integer
} deriving Show
makeLenses ''Date
type YMD = (Int, Int, Int)
toDate :: Bool -> Integer -> YMD -> Date
toDate s r (x,y,z) = Date x y z s r
toYMD :: Date -> YMD
toYMD d = (d ^. year, d ^. month, d ^. day)
isDate :: Time.Day -> Text -> Maybe Date
isDate ref t =
listToMaybe $
order $
filter valid $
map fixYear dates
where
dates :: [Date]
dates =
case Atto.parseOnly dateAvecSep t of
Left _ -> toDate False refY <$> dateSansSep t
Right ds -> toDate True refY <$> ds
order :: [Date] -> [Date]
order = sortBy (compare `on` distance)
distance :: Date -> Integer
distance d = abs (toInteger (d ^. year) - refY)
valid :: Date -> Bool
valid date =
let d = date ^. day
m = date ^. month
y = date ^. year
in m >= 1 && m <= 12
&& d >= 1 && d <= 31
&& y >= lastCentury
&& y <= (thisCentury + 100)
fixYear :: Date -> Date
fixYear d | (d ^. year) > 99 = d
| (d ^. year) > 50 = d & year +~ lastCentury
| otherwise = d & year +~ thisCentury
refY :: Integer
refY = Time.toGregorian ref ^. _1
lastCentury :: Int
lastCentury = fromInteger ((refY `div` 100) - 1) * 100
thisCentury :: Int
thisCentury = fromInteger refY `div` 100 * 100
estimateDate :: Date -> Integer
estimateDate d =
let space = max (abs (toInteger (d ^. year) - (d ^. refYear))) 20
guesses = max 1 space * 365
in if d ^. hasSep
then guesses * 4
else guesses
type Read3 a = (Maybe a, Maybe a, Maybe a)
type Arrange a = Read3 a -> Read3 a
dateSansSep :: Text -> [YMD]
dateSansSep t
| not (Text.all isDigit t) = []
| otherwise = catMaybes
[ take3 (1, 1, 2) dmy
, take3 (2, 1, 1) ymd
, take3 (2, 2, 0) ym_
, take3 (2, 2, 0) my_
, take3 (1, 2, 2) dmy
, take3 (2, 1, 2) mdy
, take3 (2, 2, 1) ymd
, take3 (2, 1, 2) ymd
, take3 (1, 1, 4) dmy
, take3 (1, 1, 4) mdy
, take3 (2, 2, 2) dmy
, take3 (2, 2, 2) mdy
, take3 (2, 2, 2) ymd
, take3 (4, 1, 1) ymd
, take3 (1, 2, 4) dmy
, take3 (1, 2, 4) mdy
, take3 (2, 1, 4) dmy
, take3 (2, 1, 4) mdy
, take3 (4, 1, 2) ymd
, take3 (4, 2, 1) ymd
, take3 (2, 2, 4) dmy
, take3 (2, 2, 4) mdy
, take3 (4, 2, 2) ymd
]
where
take3 :: (Int, Int, Int) -> Arrange Int -> Maybe YMD
take3 (x,y,z) f
| (x+y+z) /= Text.length t = Nothing
| otherwise =
let g = seq3 . f . read3
in g ( Text.take x t
, Text.take y (Text.drop x t)
, Text.drop (x+y) t
)
read3 :: (Text, Text, Text) -> Read3 Int
read3 (x, y, z) =
let r = either (const Nothing) check . Text.decimal
check (n,e) | Text.null e = Just n
| otherwise = Nothing
in (r x, r y, r z)
seq3 :: Read3 Int -> Maybe YMD
seq3 (x, y, z) = (,,) <$> x <*> y <*> z
dmy (d,m,y) = (y,m,d)
mdy (m,d,y) = (y,m,d)
ym_ (y,m,_) = (y,m, pure 1)
my_ (m,y,_) = (y,m, pure 1)
ymd = id
dateAvecSep :: Atto.Parser [YMD]
dateAvecSep = do
ds1 <- Atto.decimal
sep <- Atto.satisfy isSep
ds2 <- Atto.decimal
_ <- Atto.char sep
ds3 <- Atto.decimal
Atto.endOfInput
pure [ (ds1, ds2, ds3)
, (ds3, ds2, ds1)
, (ds3, ds1, ds2)
]
where
isSep :: Char -> Bool
isSep c = isSpace c ||
c == '/' ||
c == '\\' ||
c == '.' ||
c == '_' ||
c == '-'