hmt-base-0.20: Music/Theory/Time/Duration.hs
-- | Ordinary timing durations, in H:M:S:m (Hours:Minutes:Seconds:milliseconds)
module Music.Theory.Time.Duration where
import Text.Printf {- base -}
import qualified Data.List.Split as Split {- split -}
-- | Duration stored as /hours/, /minutes/, /seconds/ and /milliseconds/.
data Duration = Duration {hours :: Int
,minutes :: Int
,seconds :: Int
,milliseconds :: Int}
deriving (Eq)
{- | Convert fractional /seconds/ to integral /(seconds,milliseconds)/.
> s_sms 1.75 == (1,750)
-}
s_sms :: (RealFrac n,Integral i) => n -> (i,i)
s_sms s =
let s' = floor s
ms = round ((s - fromIntegral s') * 1000)
in (s',ms)
{- | Inverse of 's_sms'.
> sms_s (1,750) == 1.75
-}
sms_s :: (Integral i) => (i,i) -> Double
sms_s (s,ms) = fromIntegral s + fromIntegral ms / 1000
{- | 'Read' function for 'Duration' tuple.
The notation writes seconds fractionally, and allows hours and minutes to be elided if zero.
-}
read_duration_tuple :: String -> (Int,Int,Int,Int)
read_duration_tuple x =
let f :: (Int,Int,Double) -> (Int,Int,Int,Int)
f (h,m,s) = let (s',ms) = s_sms s in (h,m,s',ms)
in case Split.splitOneOf ":" x of
[h,m,s] -> f (read h,read m,read s)
[m,s] -> f (0,read m,read s)
[s] -> f (0,0,read s)
_ -> error "read_duration_tuple"
{- | 'Read' function for 'Duration'. Allows either @H:M:S.MS@ or @M:S.MS@ or @S.MS@.
> read_duration "01:35:05.250" == Duration 1 35 5 250
> read_duration "35:05.250" == Duration 0 35 5 250
> read_duration "05.250" == Duration 0 0 5 250
-}
read_duration :: String -> Duration
read_duration = tuple_to_duration id . read_duration_tuple
instance Read Duration where
readsPrec _ x = [(read_duration x,"")]
{- | 'Show' function for 'Duration'.
Inverse of read_duration.
Hours and minutes are always shown, even if zero.
> show_duration (Duration 1 35 5 250) == "01:35:05.250"
> show (Duration 1 15 0 000) == "01:15:00.000"
> show (Duration 0 0 3 500) == "00:00:03.500"
-}
show_duration :: Duration -> String
show_duration (Duration h m s ms) =
let f :: Int -> String
f = printf "%02d"
g = f . fromIntegral
s' = sms_s (s,ms)
in concat [g h,":",g m,":",printf "%06.3f" s']
instance Show Duration where
show = show_duration
-- | If minutes is not in (0,59) then edit hours.
normalise_minutes :: Duration -> Duration
normalise_minutes (Duration h m s ms) =
let (h',m') = m `divMod` 60
in Duration (h + h') m' s ms
-- | If seconds is not in (0,59) then edit minutes.
normalise_seconds :: Duration -> Duration
normalise_seconds (Duration h m s ms) =
let (m',s') = s `divMod` 60
in Duration h (m + m') s' ms
-- | If milliseconds is not in (0,999) then edit seconds.
normalise_milliseconds :: Duration -> Duration
normalise_milliseconds (Duration h m s ms) =
let (s',ms') = ms `divMod` 1000
in Duration h m (s + s') ms'
-- | Normalise duration so that all parts are in normal ranges.
normalise_duration :: Duration -> Duration
normalise_duration =
normalise_minutes .
normalise_seconds .
normalise_milliseconds
{- | Extract 'Duration' tuple applying filter function at each element
> duration_to_tuple id (Duration 1 35 5 250) == (1,35,5,250)
-}
duration_to_tuple :: (Int -> a) -> Duration -> (a,a,a,a)
duration_to_tuple f (Duration h m s ms) = (f h,f m,f s,f ms)
-- | Inverse of 'duration_to_tuple'.
tuple_to_duration :: (a -> Int) -> (a,a,a,a) -> Duration
tuple_to_duration f (h,m,s,ms) = Duration (f h) (f m) (f s) (f ms)
{- | Duration as fractional hours.
> duration_to_hours (read "01:35:05.250") == 1.5847916666666668
-}
duration_to_hours :: Fractional n => Duration -> n
duration_to_hours d =
let (h,m,s,ms) = duration_to_tuple fromIntegral d
in h + (m / 60) + (s / (60 * 60)) + (ms / (60 * 60 * 1000))
{- | Duration as fractional minutes.
> duration_to_minutes (read "01:35:05.250") == 95.0875
-}
duration_to_minutes :: Fractional n => Duration -> n
duration_to_minutes = (* 60) . duration_to_hours
{- | Duration as fractional seconds.
> duration_to_seconds (read "01:35:05.250") == 5705.25
-}
duration_to_seconds :: Fractional n => Duration -> n
duration_to_seconds = (* 60) . duration_to_minutes
{- | Inverse of duration_to_hours.
> hours_to_duration 1.5847916 == Duration 1 35 5 250
-}
hours_to_duration :: RealFrac a => a -> Duration
hours_to_duration n =
let r = fromIntegral :: RealFrac a => Int -> a
h = (r . floor) n
m = (n - h) * 60
(s,ms) = s_sms ((m - (r . floor) m) * 60)
in Duration (floor h) (floor m) s ms
-- | Inverse of duration_to_minutes.
minutes_to_duration :: RealFrac a => a -> Duration
minutes_to_duration n = hours_to_duration (n / 60)
-- | Inverse of duration_to_seconds.
seconds_to_duration :: RealFrac a => a -> Duration
seconds_to_duration n = minutes_to_duration (n / 60)
-- | Empty (zero) duration.
nil_duration :: Duration
nil_duration = Duration 0 0 0 0
-- | Negate the leftmost non-zero element of Duration.
negate_duration :: Duration -> Duration
negate_duration (Duration h m s ms) =
let h' = if h > 0 then -h else h
m' = if h == 0 && m > 0 then -m else m
s' = if h == 0 && m == 0 && s > 0 then -s else s
ms' = if h == 0 && m == 0 && s == 0 then -ms else ms
in Duration h' m' s' ms'
{- | Difference between two durations as a duration.
Implemented by translation to and from Rational fractional hours.
> duration_diff (Duration 1 35 5 250) (Duration 0 25 1 125) == Duration 1 10 4 125
> duration_diff (Duration 0 25 1 125) (Duration 1 35 5 250) == Duration (-1) 10 4 125
> duration_diff (Duration 0 25 1 125) (Duration 0 25 1 250) == Duration 0 0 0 (-125)
-}
duration_diff :: Duration -> Duration -> Duration
duration_diff p q =
let f = duration_to_hours :: Duration -> Rational
(p',q') = (f p,f q)
g = normalise_duration . hours_to_duration
in case compare p' q' of
LT -> negate_duration (g (q' - p'))
EQ -> nil_duration
GT -> g (p' - q')