{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE RecordWildCards #-} module Text.Time.Pretty.TimeAgo ( daysAgo , daysAgoToDays , DaysAgo(..) , timeAgo , timeAgoToDiffTime , TimeAgo(..) ) where import Data.Time import Data.Validity import GHC.Generics (Generic) import Text.Time.Pretty.Constants data DaysAgo = DaysAgo { daysAgoSign :: Ordering , daysAgoYears :: Integer , daysAgoMonths :: Integer , daysAgoWeeks :: Integer , daysAgoDays :: Integer } deriving (Show, Eq, Generic) instance Validity DaysAgo where validate da@DaysAgo {..} = mconcat [ genericValidate da , check (case daysAgoSign of EQ -> and [ daysAgoDays == 0 , daysAgoWeeks == 0 , daysAgoMonths == 0 , daysAgoYears == 0 ] _ -> any (> 0) [daysAgoDays, daysAgoWeeks, daysAgoMonths, daysAgoYears]) "the sign makes sense" , check (daysAgoYears >= 0) "years are positive" , check (daysAgoDays + daysPerWeek * daysAgoWeeks + approximateDaysPerMonth * daysAgoMonths < approximateDaysPerYear) "days, weeks and months do not sum to a year" , check (daysAgoMonths < 12) "months < 12" , check (daysAgoMonths >= 0) "months are positive" , check (daysAgoDays + daysPerWeek * daysAgoWeeks < approximateDaysPerMonth) "days and weeks do not sum to a month" , check (daysAgoWeeks < 5) "weeks < 5" , check (daysAgoWeeks >= 0) "weeks are positive" , check (daysAgoDays < 7) "days < 7" , check (daysAgoDays >= 0) "days are positive" ] daysAgo :: Integer -> DaysAgo daysAgo i = DaysAgo {..} where totalDays = abs i daysAgoSign = compare i 0 daysAgoYears = totalDays `div` approximateDaysPerYear daysLeftAfterYears = totalDays - daysAgoYears * approximateDaysPerYear daysAgoMonths = daysLeftAfterYears `div` approximateDaysPerMonth daysLeftAfterMonths = daysLeftAfterYears - daysAgoMonths * approximateDaysPerMonth daysAgoWeeks = daysLeftAfterMonths `div` daysPerWeek daysLeftAfterWeeks = daysLeftAfterMonths - daysAgoWeeks * daysPerWeek daysAgoDays = daysLeftAfterWeeks daysAgoToDays :: DaysAgo -> Integer daysAgoToDays DaysAgo {..} = (case daysAgoSign of EQ -> const 0 GT -> id LT -> negate) $ daysAgoDays + daysPerWeek * daysAgoWeeks + approximateDaysPerMonth * daysAgoMonths + approximateDaysPerYear * daysAgoYears data TimeAgo = TimeAgo { timeAgoSign :: Ordering , timeAgoDaysAgo :: DaysAgo , timeAgoHours :: Integer , timeAgoMinutes :: Integer , timeAgoSeconds :: Integer , timeAgoPicoSeconds :: Integer } deriving (Show, Eq, Generic) instance Validity TimeAgo where validate ta@TimeAgo {..} = mconcat [ genericValidate ta , check (case timeAgoSign of EQ -> and [ daysAgoToDays timeAgoDaysAgo == 0 , timeAgoHours == 0 , timeAgoMinutes == 0 , timeAgoSeconds == 0 , timeAgoPicoSeconds == 0 ] _ -> any (> 0) [ daysAgoToDays timeAgoDaysAgo , timeAgoHours , timeAgoMinutes , timeAgoSeconds , timeAgoPicoSeconds ]) "the sign makes sense" , check (daysAgoSign timeAgoDaysAgo /= LT) "The days ago are not negative" , check (timeAgoHours < hoursPerDay) "hours < 24" , check (timeAgoHours >= 0) "hours are positive" , check (timeAgoMinutes < minutesPerHour) "minutes < 60" , check (timeAgoMinutes >= 0) "minutes are positive" , check (timeAgoSeconds < secondsPerMinute) "seconds < 60" , check (timeAgoSeconds >= 0) "seconds are positive" , check (timeAgoPicoSeconds < picoSecondsPerSecond) "picoseconds < 1E12" , check (timeAgoPicoSeconds >= 0) "picoseconds are positive" ] timeAgo :: NominalDiffTime -> TimeAgo timeAgo dt = TimeAgo {..} where timeAgoSign = compare dt 0 timeAgoPicoSeconds = totalPicoSecondsAgo - picoSecondsPerSecond * totalSecondsAgo timeAgoSeconds = totalSecondsAgo - secondsPerMinute * totalMinutesAgo timeAgoMinutes = totalMinutesAgo - minutesPerHour * totalHoursAgo timeAgoHours = totalHoursAgo - hoursPerDay * totalDaysAgo timeAgoDaysAgo = daysAgo totalDaysAgo totalPicoSecondsAgo = floor $ absDt * fromIntegral (picoSecondsPerSecond :: Integer) totalSecondsAgo = floor absDt :: Integer totalMinutesAgo = floor $ absDt / fromIntegral (secondsPerMinute :: Integer) totalHoursAgo = floor $ absDt / fromIntegral (minutesPerHour * secondsPerMinute :: Integer) totalDaysAgo = floor $ absDt / fromIntegral (hoursPerDay * minutesPerHour * secondsPerMinute :: Integer) absDt = abs dt timeAgoToDiffTime :: TimeAgo -> NominalDiffTime timeAgoToDiffTime TimeAgo {..} = (/ fromIntegral (picoSecondsPerSecond :: Integer)) $ realToFrac $ (case timeAgoSign of EQ -> const 0 GT -> id LT -> negate) (timeAgoPicoSeconds + picoSecondsPerSecond * (timeAgoSeconds + secondsPerMinute * (timeAgoMinutes + minutesPerHour * (timeAgoHours + hoursPerDay * (daysAgoToDays timeAgoDaysAgo)))))