flick-duration (empty) → 1.0.0
raw patch · 7 files changed
+489/−0 lines, 7 filesdep +QuickCheckdep +basedep +clocksetup-changed
Dependencies added: QuickCheck, base, clock, flick-duration, hspec
Files
- ChangeLog.md +3/−0
- LICENSE +30/−0
- README.md +1/−0
- Setup.hs +2/−0
- flick-duration.cabal +55/−0
- src/Data/Duration.hs +292/−0
- test/Spec.hs +106/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for flick-duration++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Christopher Lord (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Christopher Lord nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,1 @@+# flick-duration
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ flick-duration.cabal view
@@ -0,0 +1,55 @@+-- This file has been generated from package.yaml by hpack version 0.28.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 22cbaca7047b68aa7c84e97decb74683a65efbf5d5855c4dba496881aac47284++name: flick-duration+version: 1.0.0+synopsis: work with durations of time using the Flick as the smallest unit+description: Please see the README on GitHub at <https://github.com/pliosoft/flick-duration#readme>+category: Time+homepage: https://github.com/pliosoft/flick-duration#readme+bug-reports: https://github.com/pliosoft/flick-duration/issues+author: Christopher Lord+maintainer: christopher@pliosoft.com+copyright: (C) Christopher Lord (Pliosoft), All Rights Reserved+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10+extra-source-files:+ ChangeLog.md+ README.md++source-repository head+ type: git+ location: https://github.com/pliosoft/flick-duration++library+ exposed-modules:+ Data.Duration+ other-modules:+ Paths_flick_duration+ hs-source-dirs:+ src+ build-depends:+ base >=4.7 && <5+ , clock >=0.5 && <1+ default-language: Haskell2010++test-suite flick-duration-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_flick_duration+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ QuickCheck+ , base >=4.7 && <5+ , clock >=0.5 && <1+ , flick-duration+ , hspec+ default-language: Haskell2010
+ src/Data/Duration.hs view
@@ -0,0 +1,292 @@++{-|+Module : Data.FlickDuration+Copyright : (c) 2018 Christopher C Lord, Pliosoft+License : BSD Style+Stability : experimental+Maintainer : christopher@pliosoft.com+Portability : GHC++Use a to contain some number of flicks, and provide standard numerical operations.++Based on idea from <https:www.facebook.com/christopher.horvath.395/posts/1157292757692660 ChristopherHorvath>+-}+module Data.Duration+ (+ -- * The 'Duration' type+ Duration++ -- * Constant Ratios+ , flicksPerMillisecond+ , flicksPerSecond+ , flicksPerMinute+ , flickRatio+ , durationToRationalFlicks+ , durationToRationalSeconds++ -- * 'Duration' to various human units+ , durationAsHours+ , durationAsNanoseconds+ , durationAsMicroseconds+ , durationAsMilliseconds+ , durationAsMinutes+ , durationAsSeconds+ , durationAsHms++ -- * Creating 'Duration' from various human units+ , durationFromFlicks+ , durationFromHours+ , durationFromNanoseconds+ , durationFromMicroseconds+ , durationFromMilliseconds+ , durationFromMinutes+ , durationFromSeconds++ -- * 'Duration' from wavelength at some frequency, and vice versa+ , durationOfOneBeatAtBPM+ , durationOfOneCycleAtHz+ , frequencyInHzForWavePeriod++ -- * 'Duration' manipulations+ , durationOfRepeatedDuration+ , durationFromDividingDuration+ , durationIntoDuration+ , negateDuration+ , compareDurationsWithEpsilon++ -- * Thread delay+ , delayThreadByDuration++ -- * Measuring delay with the system clock+ , StartOfDuration+ , startMeasuring+ , durationSince+ , measurementEpsilon+ )+where++import Prelude+import Data.Ratio+import Data.Semigroup+import Data.Maybe+import System.Clock+import Control.Concurrent(threadDelay)+import Control.Monad ((>>), when)++-- | Flicks are a small unit of time that are very evenly divisible by common file format time durations; i.e., for common durations they will have no rounding.+-- Even if there is rounding, it is a very short amount of time -- 1.417 nanoseconds, so rounding is typically not a concern.+flicksPerMillisecond :: Num a => a+flicksPerMillisecond = 705600++-- | How many flicks are in a second+flicksPerSecond :: Num a => a+flicksPerSecond = 705600000++-- | How many flicks are in a minute+flicksPerMinute :: Num a => a+flicksPerMinute = 60 * 705600000++-- | 'Ratio' of seconds to flicks. for example, 100 seconds of flicks is @(100 * flickRatio)@+flickRatio :: Fractional a => a+flickRatio = recip flicksPerSecond++-- | Represents a span of time using some number of flicks. To create a 'Duration' use:+--+-- * 'durationFromFlicks'+-- * 'durationFromHours'+-- * 'durationFromMicroseconds'+-- * 'durationFromMilliseconds'+-- * 'durationFromMinutes'+-- * 'durationFromSeconds'+--+-- 'Duration' can be concatinated with 'mappend' or the '(<>)' operator.+newtype Duration = Duration { fromDuration :: Integer }+ deriving (Eq, Ord, Show)++instance Semigroup Duration where+ a <> b = Duration $ fromDuration a + fromDuration b++instance Monoid Duration where+ mempty = Duration 0+ mappend = (<>)++{-+Can't have Num instance since it makes little sense to multiply durations Durations are not a ring. But we can have monoid (and group?) instances.+instance Num Duration where+ a + b = Duration $ fromDuration a + fromDuration b+ a * b = Duration $ fromDuration a * fromDuration b+ abs a = Duration $ abs (fromDuration a)+ signum a = Duration $ signum (fromDuration a)+ fromInteger a = Duration (fromInteger a)+ negate a = Duration (negate (fromDuration a))+-}++-- | Convert a 'Duration' into flicks, stored in a 'Rational'+durationToRationalFlicks :: Duration -> Rational+durationToRationalFlicks d = toRational (fromDuration d)++-- | Convert a 'Duration' into seconds, stored in a 'Rational'+durationToRationalSeconds :: Duration -> Rational+durationToRationalSeconds d = flickRatio * durationToRationalFlicks d++-- | given some 'Duration' d1, how many times can we iterate that duration into another duration+durationIntoDuration :: Duration -> Duration -> Maybe Rational+durationIntoDuration _ d2 | fromDuration d2 == 0 = Nothing+durationIntoDuration d1 d2 = Just $ fromDuration d1 % fromDuration d2++-- | Repeat a 'Duration' some rational number of times to produce the total duration+durationOfRepeatedDuration :: Duration -> Rational -> Duration+durationOfRepeatedDuration d r = Duration $ truncate (durationToRationalFlicks d * r)++-- | take a 'Duration' and divide it into equal pieces of some length (the result)+durationFromDividingDuration :: Duration -> Rational -> Maybe Duration+durationFromDividingDuration d r = if r == 0 then Nothing else Just $ durationOfRepeatedDuration d (recip r)++-- | Given a number of flicks, produce a 'Duration'+durationFromFlicks :: Integral a => a -> Duration+durationFromFlicks i = Duration (fromIntegral i)++-- | Given a number of nanoseconds, produce a 'Duration'.+-- Note that flicks are larger than nanoseconds, so rounding will occur+durationFromNanoseconds :: Rational -> Duration+durationFromNanoseconds i = durationFromMicroseconds (i / 1000)++-- | Given a number of microseconds, produce a 'Duration'+durationFromMicroseconds :: Rational -> Duration+durationFromMicroseconds i = durationFromMilliseconds (i / 1000)++-- | Given a number of milliseconds, produce a 'Duration'+durationFromMilliseconds :: Rational -> Duration+durationFromMilliseconds i = Duration (truncate (i * flicksPerMillisecond))++-- | Given a number of seconds, produce a 'Duration'+durationFromSeconds :: Rational -> Duration+durationFromSeconds i = Duration (truncate (i * flicksPerSecond))++-- | Given a number of minutes, produce a 'Duration'+durationFromMinutes :: Rational -> Duration+durationFromMinutes i = Duration (truncate (i * flicksPerMinute))++-- | Given a number of hours, produce a 'Duration'+durationFromHours :: Rational -> Duration+durationFromHours i = durationFromMinutes (i * 60)++-- | Given an epsilon e and two durations, will determine their 'Ord' with respect to the epsilon+compareDurationsWithEpsilon :: Duration -> Duration -> Duration -> Ordering+compareDurationsWithEpsilon e a b =+ let diff = (fromDuration a) - (fromDuration b)+ in if abs diff <= fromDuration e+ then EQ+ else if diff < 0+ then LT+ else GT++-- | given some frequency in cycles per second, produce the 'Duration' of a single wavelength's period+durationOfOneCycleAtHz :: Rational -> Maybe Duration+durationOfOneCycleAtHz hz =+ if hz <= 0+ then Nothing+ else durationFromDividingDuration (durationFromSeconds 1) hz++-- | given some frequency in beats per minute, produce the 'Duration' between beats+durationOfOneBeatAtBPM :: Rational -> Maybe Duration+durationOfOneBeatAtBPM bpm =+ if bpm <= 0+ then Nothing+ else durationFromDividingDuration (durationFromMinutes 1) bpm++-- | If 'Duration' d is the time for one period of a wave, produce the corresponding frequency in hz.+frequencyInHzForWavePeriod :: Duration -> Maybe Rational+frequencyInHzForWavePeriod = durationIntoDuration (durationFromSeconds 1)++-- | given some 'Duration', represent it as a count of seconds+durationAsSeconds :: Duration -> Rational+durationAsSeconds d = durationAsMilliseconds d / 1000++-- | given some 'Duration', represent it as a count of minutes+durationAsMinutes :: Duration -> Rational+durationAsMinutes d = durationAsSeconds d / 60++-- | given some 'Duration', represent it as a count of hours+durationAsHours :: Duration -> Rational+durationAsHours d = durationAsMinutes d / 60++-- | given some 'Duration', represent it as a count of milliseconds+durationAsMilliseconds :: Duration -> Rational+durationAsMilliseconds d = fromDuration d % flicksPerMillisecond++-- | given some 'Duration', represent it as a count of microseconds+durationAsMicroseconds :: Duration -> Rational+durationAsMicroseconds d = fromRational 1000 * durationAsMilliseconds d++-- | given some 'Duration', represent it as a count of nanoseconds+durationAsNanoseconds :: Duration -> Rational+durationAsNanoseconds d = fromRational 1000 * durationAsMicroseconds d++-- | given some 'Duration' produce a negative version+negateDuration :: Duration -> Duration+negateDuration d = Duration (negate (fromDuration d))++-- | Given two durations, produce the larger of the two+maximumOfDurations :: Duration -> Duration -> Duration+maximumOfDurations d1 d2 =+ if d1 > d2 then d1 else d2++-- | Given two durations, produce the smaller of the two+minimumOfDurations :: Duration -> Duration -> Duration+minimumOfDurations d1 d2 =+ if d1 < d2 then d1 else d2++-- | present a 'Duration' as a count of hours, minutes, seconds, ms, us+durationAsHms :: Duration -> (Integer, Integer, Integer, Integer, Integer)+durationAsHms d =+ let (h,hr) = fromDuration d `divMod` fromDuration (durationFromHours 1)+ (m,mr) = hr `divMod` fromDuration (durationFromMinutes 1)+ (s,sr) = mr `divMod` fromDuration (durationFromSeconds 1)+ (ms,msr) = sr `divMod` fromDuration (durationFromMilliseconds 1)+ (us,usr) = msr `divMod` fromDuration (durationFromMicroseconds 1)+ in (h,m,s,ms,us)++-- | Suspend currently running thread for a 'Duration' of time.+--+-- Provides no exact guarantee on when the thread will be resumed after the delay.+delayThreadByDuration :: Duration -> IO ()+delayThreadByDuration d = do+ let maxMicroseconds = durationFromMicroseconds (fromIntegral (maxBound :: Int))+ let w = min d maxMicroseconds+ threadDelay (truncate (durationAsMicroseconds (min d maxMicroseconds)))+ when (w /= d) (delayThreadByDuration (d <> negateDuration w))++-- | Holds implementation-defined start of time measurment for 'startMeasuring'+newtype StartOfDuration = StartOfDuration { toDurationFromOrigin :: Duration }++-- | Helper for internal math+timespecToDuration :: TimeSpec -> Duration+timespecToDuration ts = durationFromSeconds (fromIntegral (sec ts)) <> durationFromNanoseconds (fromIntegral (nsec ts))++-- | start measuring a duration+startMeasuring :: IO StartOfDuration+startMeasuring = do+ now <- getTime Monotonic+ let d1 = timespecToDuration now+ return (StartOfDuration d1)++-- | How long since the start of measuring has it mean, plus or minus 'measurementEpsilon'+durationSince :: StartOfDuration -> IO Duration+durationSince s = do+ now <- getTime Monotonic+ let d1 = toDurationFromOrigin s+ let d2 = timespecToDuration now+ return (Duration ((fromDuration d2) - (fromDuration d1)))++-- | About how accurate is are the methods 'durationSince' and 'startMeasuring'+measurementEpsilon :: IO Duration+measurementEpsilon = return (durationFromMilliseconds 100)+ {- ugg, this does not seem to give a reasonable e+ do+ res <- getRes Monotonic+ let rres = timespecToDuration res+ putStrLn (show (durationToRationalSeconds rres))+ return $ rres+ -}+
+ test/Spec.hs view
@@ -0,0 +1,106 @@+import Test.Hspec+import Test.QuickCheck+import Data.Duration+import Data.Semigroup+import Data.Maybe (fromJust)+import Data.Ratio++main :: IO ()+main = hspec $ do+ describe "Data.FlickDuration" $ do+ describe "Core" $ do+ it "should have correct flicks for a second" $+ durationFromSeconds 1+ `shouldBe` (durationFromFlicks 705600000)+ it "microseconds should round correct" $+ durationFromMicroseconds (1 % 10)+ `shouldBe` durationFromFlicks 70+ it "should do basic combinations" $+ (durationFromSeconds 12 <> durationFromHours 1 <> durationFromMinutes 12)+ `shouldBe` (durationFromFlicks 3056659200000)+ it "should do basic convert to hms correctly" $+ durationAsHms (durationFromSeconds 12 <> durationFromHours 1 <> durationFromMinutes 12)+ `shouldBe` (1, 12, 12, 0, 0)+ it "should wrap larger values" $+ durationAsHms (durationFromSeconds 1201 <> durationFromHours 2 <> durationFromMinutes 18012 <> durationFromMilliseconds 123947 <> durationFromMicroseconds 97435)+ `shouldBe` (302, 34, 5, 44, 435)+ it "should convert correctly to microseconds" $ property $+ \x -> durationAsMicroseconds (durationFromMicroseconds (fromIntegral (x :: Integer)))+ `shouldSatisfy` (\y -> abs (y - fromIntegral x) < 0.01)+ it "should convert correctly to milliseconds" $ property $+ \x -> durationAsMilliseconds (durationFromMilliseconds (fromIntegral (x :: Integer)))+ `shouldBe` (fromIntegral x)+ it "should convert correctly to seconds" $ property $+ \x -> durationAsSeconds (durationFromSeconds (fromIntegral (x :: Integer)))+ `shouldBe` (fromIntegral x)+ it "should convert correctly to minutes" $ property $+ \x -> durationAsMinutes (durationFromMinutes (fromIntegral (x :: Integer)))+ `shouldBe` (fromIntegral x)+ it "should convert correctly to hours" $ property $+ \x -> durationAsHours (durationFromHours (fromIntegral (x :: Integer)))+ `shouldBe` (fromIntegral x)+ it "should compute multiple durations" $+ durationOfRepeatedDuration (durationFromSeconds 12) 10+ `shouldBe` durationFromSeconds (12 * 10)+ it "should not divide by zero" $+ durationFromDividingDuration (durationFromSeconds 12) 0+ `shouldBe` Nothing+ it "durationIntoDuration" $+ durationIntoDuration (durationFromSeconds 1) (durationFromMicroseconds 100)+ `shouldBe` Just 10000+ it "handles iterating zero duration" $+ durationIntoDuration (durationFromSeconds 1) (durationFromSeconds 0)+ `shouldBe` Nothing+ it "should divide durations into parts" $+ durationFromDividingDuration (durationFromSeconds 12) 6+ `shouldBe` (Just (durationFromSeconds 2))+ it "mconcat array of durations" $+ mconcat [durationFromMilliseconds 100, durationFromMilliseconds 200, durationFromMilliseconds 40, durationFromMilliseconds 60]+ `shouldBe` durationFromMilliseconds 400+ it "compare durations with eps LT" $+ compareDurationsWithEpsilon (durationFromMilliseconds 1) (durationFromMilliseconds 100) (durationFromMilliseconds 110)+ `shouldBe` LT+ it "compare durations with eps GT" $+ compareDurationsWithEpsilon (durationFromMilliseconds 1) (durationFromMilliseconds 100) (durationFromMilliseconds 11)+ `shouldBe` GT+ it "compare durations with eps EQ" $+ compareDurationsWithEpsilon (durationFromMilliseconds 1) (durationFromMilliseconds 100) (durationFromMilliseconds 101)+ `shouldBe` EQ+ it "min of two durations" $+ (min (durationFromMilliseconds 100) (durationFromMicroseconds 1000), max (durationFromMilliseconds 100) (durationFromMicroseconds 1000))+ `shouldBe` (durationFromMicroseconds 1000, durationFromMilliseconds 100)++ describe "Sleep and Measure" $ do+ it "should sleep for about the right amount of time" $ do+ e <- measurementEpsilon+ b <- startMeasuring+ let specDelay = durationFromMilliseconds 120+ delayThreadByDuration specDelay+ duration <- durationSince b+ duration `shouldSatisfy` (\y -> EQ == compareDurationsWithEpsilon e specDelay y)++ describe "Frequency" $ do+ it "should not support negative bpm" $+ durationOfOneBeatAtBPM (-110)+ `shouldBe` Nothing+ it "should compute correct duration of bpm" $+ durationOfOneBeatAtBPM 120+ `shouldBe` Just (durationFromMilliseconds 500)+ it "should get the exact period and frequency of bpm for particular value" $+ (maybe Nothing frequencyInHzForWavePeriod (durationOfOneBeatAtBPM 100))+ `shouldBe` (Just (100 % 60))+ it "should get the exact period and frequency of hz for particular value" $+ (maybe Nothing frequencyInHzForWavePeriod (durationOfOneCycleAtHz 100))+ `shouldBe` (Just 100)+ it "should get the approx period and frequency for arbitrary hz" $ property $+ \x -> (maybe Nothing frequencyInHzForWavePeriod (durationOfOneCycleAtHz (fromIntegral (x :: Integer))))+ `shouldSatisfy` (\y -> if x <= 0 then y == Nothing else let fy = fromJust y in abs (fromIntegral x - fy) < 1)+ it "should exclude zero hz" $+ durationOfOneCycleAtHz 0+ `shouldBe` Nothing+ it "should compute correct duration of cycle" $ property $+ \x -> if x > 0 then durationOfOneCycleAtHz x `shouldBe` Just (durationFromSeconds (recip x)) else return ()+ it "should compute correct hz" $+ Just (durationFromMilliseconds 500)+ `shouldBe` (durationOfOneCycleAtHz 2)+