From 67e42f9da90822b4bfe18f0020a287141defb63d Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Thu, 14 Jul 2016 14:43:30 -0400 Subject: [PATCH 01/11] Added dynamic prefix selection. Fixes #163. --- src/Numeric/Units/Dimensional/SIUnits.hs | 63 ++++++++++++++++++- src/Numeric/Units/Dimensional/UnitNames.hs | 5 +- .../Units/Dimensional/UnitNames/Internal.hs | 46 +++++++------- 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/src/Numeric/Units/Dimensional/SIUnits.hs b/src/Numeric/Units/Dimensional/SIUnits.hs index 8535d75c..603f852b 100644 --- a/src/Numeric/Units/Dimensional/SIUnits.hs +++ b/src/Numeric/Units/Dimensional/SIUnits.hs @@ -57,19 +57,22 @@ module Numeric.Units.Dimensional.SIUnits -- $submultiples deci, centi, milli, micro, nano, pico, femto, atto, zepto, yocto, -- $reified-prefixes - Prefix, applyPrefix, siPrefixes + Prefix, applyPrefix, applyOptionalPrefix, siPrefixes, appropriatePrefix, withAppropriatePrefix, appropriatePrefix', withAppropriatePrefix' ) where +import Control.Monad (join) import Data.Ratio +import Data.List (sortBy, find) +import Data.Ord (comparing, Down(..)) import Numeric.Units.Dimensional import Numeric.Units.Dimensional.Quantities -import Numeric.Units.Dimensional.UnitNames (Prefix, siPrefixes) +import Numeric.Units.Dimensional.UnitNames (Prefix, siPrefixes, scaleExponent) import qualified Numeric.Units.Dimensional.UnitNames as N import Numeric.Units.Dimensional.UnitNames.Internal (ucum, ucumMetric) import qualified Numeric.Units.Dimensional.UnitNames.Internal as I import Numeric.NumType.DK.Integers ( pos3 ) -import Prelude ( Eq(..), ($), Num, Fractional, Floating, otherwise, error) +import Prelude ( Eq(..), ($), (.), Num, Fractional, Floating, RealFrac(..), Maybe(..), otherwise, error, Ord(..), fst, snd, Int, Bool, fmap, mod, (&&)) import qualified Prelude {- $multiples @@ -109,12 +112,18 @@ yotta = applyMultiple I.yotta Then the submultiples. -} +-- | Applies a 'Prefix' to a 'Metric' 'Unit', creating a 'NonMetric' unit. applyPrefix :: (Fractional a) => Prefix -> Unit 'Metric d a -> Unit 'NonMetric d a applyPrefix p u = mkUnitQ n' x u where n' = N.applyPrefix p (name u) x = N.scaleFactor p +-- | Applies an optional 'Prefix' to a 'Metric' 'Unit', creating a 'NonMetric' unit. +applyOptionalPrefix :: (Fractional a) => Maybe Prefix -> Unit 'Metric d a -> Unit 'NonMetric d a +applyOptionalPrefix Nothing = weaken +applyOptionalPrefix (Just p) = applyPrefix p + deci, centi, milli, micro, nano, pico, femto, atto, zepto, yocto :: Fractional a => Unit 'Metric d a -> Unit 'NonMetric d a deci = applyPrefix I.deci @@ -135,6 +144,54 @@ list of all prefixes defined by the SI. -} +-- | Selects the appropriate 'Prefix' to use with a 'Metric' unit when using it to display +-- a particular 'Quantity', or 'Nothing' if the supplied unit should be used without a prefix. +-- +-- The appropriate prefix is defined to be the largest prefix such that the resulting value +-- of the quantity, expressed in the prefixed unit, is greater than or equal to one. +appropriatePrefix :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Maybe Prefix +appropriatePrefix u q = selectPrefix (<= e) + where + val = q /~ u + e = Prelude.floor $ Prelude.logBase 10 val :: Prelude.Int + +-- | Selects the appropriate 'Prefix' to use with a 'Metric' unit when using it to display +-- a particular 'Quantity', or 'Nothing' if the supplied unit should be used without a prefix. +-- +-- The appropriate prefix is defined to be the largest prefix such that the resulting value +-- of the quantity, expressed in the prefixed unit, is greater than or equal to one. Only those prefixes +-- whose 'scaleExponent' is a multiple of @3@ are considered. +appropriatePrefix' :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Maybe Prefix +appropriatePrefix' u q = selectPrefix (\x -> x `mod` 3 == 0 && x <= e) + where + val = q /~ u + e = Prelude.floor $ Prelude.logBase 10 val :: Prelude.Int + +-- Selects the first prefix in the list of prefix candidates whose scale exponent matches the supplied predicate. +selectPrefix :: (Int -> Bool) -> Maybe Prefix +selectPrefix p = join $ fmap snd $ find (p . fst) prefixCandidates + +-- This is a list of candidate prefixes and the least scale exponent at which each applies. +prefixCandidates :: [(Int, Maybe Prefix)] +prefixCandidates = sortBy (comparing $ Down . fst) $ (0, Nothing) : fmap (\x -> (scaleExponent x, Just x)) siPrefixes + +-- | Constructs a version of a 'Metric' unit, by possibly applying a 'Prefix' to it, appropriate +-- for display of a particular 'Quantity'. +-- +-- The appropriate prefix is defined to be the largest prefix such that the resulting value +-- of the quantity, expressed in the prefixed unit, is greater than or equal to one. +withAppropriatePrefix :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Unit 'NonMetric d a +withAppropriatePrefix u q = applyOptionalPrefix (appropriatePrefix u q) u + +-- | Constructs a version of a 'Metric' unit, by possibly applying a 'Prefix' to it, appropriate +-- for display of a particular 'Quantity'. +-- +-- The appropriate prefix is defined to be the largest prefix such that the resulting value +-- of the quantity, expressed in the prefixed unit, is greater than or equal to one. Only those prefixes +-- whose 'scaleExponent' is a multiple of @3@ are considered. +withAppropriatePrefix' :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Unit 'NonMetric d a +withAppropriatePrefix' u q = applyOptionalPrefix (appropriatePrefix' u q) u + {- $base-units These are the base units from section 4.1. To avoid a myriad of one-letter functions that would doubtlessly cause clashes diff --git a/src/Numeric/Units/Dimensional/UnitNames.hs b/src/Numeric/Units/Dimensional/UnitNames.hs index b2cf9fd7..a5f894e0 100644 --- a/src/Numeric/Units/Dimensional/UnitNames.hs +++ b/src/Numeric/Units/Dimensional/UnitNames.hs @@ -23,7 +23,7 @@ module Numeric.Units.Dimensional.UnitNames -- * Standard Names baseUnitName, siPrefixes, nOne, -- * Inspecting Prefixes - prefixName, scaleFactor, + prefixName, scaleExponent, scaleFactor, -- * Convenience Type Synonyms for Unit Name Transformations UnitNameTransformer, UnitNameTransformer2, -- * Forgetting Unwanted Phantom Types @@ -35,3 +35,6 @@ where import Numeric.Units.Dimensional.UnitNames.Internal import Numeric.Units.Dimensional.Variants import Prelude hiding ((*), (/), (^), product) + +scaleFactor :: Prefix -> Rational +scaleFactor p = 10 ^^ (scaleExponent p) diff --git a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs index 9f8d00dd..0128d4fa 100644 --- a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs +++ b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs @@ -141,12 +141,12 @@ data Prefix = Prefix -- | The name of a metric prefix. prefixName :: PrefixName, -- | The scale factor denoted by a metric prefix. - scaleFactor :: Rational + scaleExponent :: Int } deriving (Eq, Data, Typeable, Generic) instance Ord Prefix where - compare = comparing scaleFactor + compare = comparing scaleExponent instance NFData Prefix where -- instance is derived from Generic instance @@ -190,27 +190,27 @@ baseUnitNames :: [UnitName 'NonMetric] baseUnitNames = [weaken nMeter, nKilogram, weaken nSecond, weaken nAmpere, weaken nKelvin, weaken nMole, weaken nCandela] deka, hecto, kilo, mega, giga, tera, peta, exa, zetta, yotta :: Prefix -deka = prefix "da" "da" "deka" 1e1 -hecto = prefix "h" "h" "hecto" 1e2 -kilo = prefix "k" "k" "kilo" 1e3 -mega = prefix "M" "M" "mega" 1e6 -giga = prefix "G" "G" "giga" 1e9 -tera = prefix "T" "T" "tera" 1e12 -peta = prefix "P" "P" "peta" 1e15 -exa = prefix "E" "E" "exa" 1e18 -zetta = prefix "Z" "Z" "zetta" 1e21 -yotta = prefix "Y" "Y" "yotta" 1e24 +deka = prefix "da" "da" "deka" 1 +hecto = prefix "h" "h" "hecto" 2 +kilo = prefix "k" "k" "kilo" 3 +mega = prefix "M" "M" "mega" 6 +giga = prefix "G" "G" "giga" 9 +tera = prefix "T" "T" "tera" 12 +peta = prefix "P" "P" "peta" 15 +exa = prefix "E" "E" "exa" 18 +zetta = prefix "Z" "Z" "zetta" 21 +yotta = prefix "Y" "Y" "yotta" 24 deci, centi, milli, micro, nano, pico, femto, atto, zepto, yocto :: Prefix -deci = prefix "d" "d" "deci" 1e-1 -centi = prefix "c" "c" "centi" 1e-2 -milli = prefix "m" "m" "milli" 1e-3 -micro = prefix "u" "μ" "micro" 1e-6 -nano = prefix "n" "n" "nano" 1e-9 -pico = prefix "p" "p" "pico" 1e-12 -femto = prefix "f" "f" "femto" 1e-15 -atto = prefix "a" "a" "atto" 1e-18 -zepto = prefix "z" "z" "zepto" 1e-21 -yocto = prefix "y" "y" "yocto" 1e-24 +deci = prefix "d" "d" "deci" $ -1 +centi = prefix "c" "c" "centi" $ -2 +milli = prefix "m" "m" "milli" $ -3 +micro = prefix "u" "μ" "micro" $ -6 +nano = prefix "n" "n" "nano" $ -9 +pico = prefix "p" "p" "pico" $ -12 +femto = prefix "f" "f" "femto" $ -15 +atto = prefix "a" "a" "atto" $ -18 +zepto = prefix "z" "z" "zepto" $ -21 +yocto = prefix "y" "y" "yocto" $ -24 -- | A list of all 'Prefix'es defined by the SI. siPrefixes :: [Prefix] @@ -319,7 +319,7 @@ instance HasInterchangeName (UnitName m) where in InterchangeName { name = n', authority = authority . interchangeName $ n, I.isAtomic = False } interchangeName (Weaken n) = interchangeName n -prefix :: String -> String -> String -> Rational -> Prefix +prefix :: String -> String -> String -> Int -> Prefix prefix i a f q = Prefix n q where n = NameAtom (InterchangeName i UCUM True) a f From 9799f385b586ddc4528983ca513a84338f33f38d Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Thu, 14 Jul 2016 14:47:34 -0400 Subject: [PATCH 02/11] Changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff961030..c0a22fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ vNext * Added an `AEq` instance for `Quantity`. * Exposed the name of an 'AnyUnit' without promoting it to a 'Unit' first. * Exposed a way to convert atomic 'UnitName's back into 'NameAtom's. +* Added dynamic selection of metric prefixes based on the magnitude of a quantity to be displayed. * Added the `btu`, a unit of energy. * Added the `gauss`, a unit of magnetic flux density. * Added the `angstrom`, a unit of length. From 3b919c85e223ea27dc8808e33b5cf15c5d976147 Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Fri, 15 Jul 2016 09:57:40 -0400 Subject: [PATCH 03/11] Relaxed type of appropriatePrefix to allow its use in more complicated scenarios. --- src/Numeric/Units/Dimensional/SIUnits.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Numeric/Units/Dimensional/SIUnits.hs b/src/Numeric/Units/Dimensional/SIUnits.hs index 603f852b..0f18361e 100644 --- a/src/Numeric/Units/Dimensional/SIUnits.hs +++ b/src/Numeric/Units/Dimensional/SIUnits.hs @@ -149,7 +149,10 @@ list of all prefixes defined by the SI. -- -- The appropriate prefix is defined to be the largest prefix such that the resulting value -- of the quantity, expressed in the prefixed unit, is greater than or equal to one. -appropriatePrefix :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Maybe Prefix +-- +-- Note that the supplied prefix need not be 'Metric'. This is intended for use to compute a prefix to insert +-- somewhere in the denominator of a composite (and hence 'NonMetric') unit. +appropriatePrefix :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Maybe Prefix appropriatePrefix u q = selectPrefix (<= e) where val = q /~ u @@ -161,7 +164,10 @@ appropriatePrefix u q = selectPrefix (<= e) -- The appropriate prefix is defined to be the largest prefix such that the resulting value -- of the quantity, expressed in the prefixed unit, is greater than or equal to one. Only those prefixes -- whose 'scaleExponent' is a multiple of @3@ are considered. -appropriatePrefix' :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Maybe Prefix +-- +-- Note that the supplied prefix need not be 'Metric'. This is intended for use to compute a prefix to insert +-- somewhere in the denominator of a composite (and hence 'NonMetric') unit. +appropriatePrefix' :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Maybe Prefix appropriatePrefix' u q = selectPrefix (\x -> x `mod` 3 == 0 && x <= e) where val = q /~ u From b7bc2cc937471c7db869ef3ea55d405f5cbf1bc0 Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Mon, 18 Jul 2016 16:12:31 -0400 Subject: [PATCH 04/11] Changed handling when value is smaller than the smallest unit to be consistent with what happens when it is larger than the largest unit. --- src/Numeric/Units/Dimensional/SIUnits.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Numeric/Units/Dimensional/SIUnits.hs b/src/Numeric/Units/Dimensional/SIUnits.hs index 0f18361e..ff98dafa 100644 --- a/src/Numeric/Units/Dimensional/SIUnits.hs +++ b/src/Numeric/Units/Dimensional/SIUnits.hs @@ -62,9 +62,10 @@ module Numeric.Units.Dimensional.SIUnits where import Control.Monad (join) -import Data.Ratio import Data.List (sortBy, find) +import Data.Maybe (maybe) import Data.Ord (comparing, Down(..)) +import Data.Ratio import Numeric.Units.Dimensional import Numeric.Units.Dimensional.Quantities import Numeric.Units.Dimensional.UnitNames (Prefix, siPrefixes, scaleExponent) @@ -175,7 +176,7 @@ appropriatePrefix' u q = selectPrefix (\x -> x `mod` 3 == 0 && x <= e) -- Selects the first prefix in the list of prefix candidates whose scale exponent matches the supplied predicate. selectPrefix :: (Int -> Bool) -> Maybe Prefix -selectPrefix p = join $ fmap snd $ find (p . fst) prefixCandidates +selectPrefix p = maybe (Just . Prelude.head $ siPrefixes) snd $ find (p . fst) prefixCandidates -- This is a list of candidate prefixes and the least scale exponent at which each applies. prefixCandidates :: [(Int, Maybe Prefix)] From 173669852ea6d7d291e1ebc4fa0c6e31bfb570b1 Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Mon, 25 Jul 2016 21:06:05 -0400 Subject: [PATCH 05/11] Added very important absolute value to unit selection. --- src/Numeric/Units/Dimensional/SIUnits.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Numeric/Units/Dimensional/SIUnits.hs b/src/Numeric/Units/Dimensional/SIUnits.hs index ff98dafa..392372cc 100644 --- a/src/Numeric/Units/Dimensional/SIUnits.hs +++ b/src/Numeric/Units/Dimensional/SIUnits.hs @@ -171,7 +171,7 @@ appropriatePrefix u q = selectPrefix (<= e) appropriatePrefix' :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Maybe Prefix appropriatePrefix' u q = selectPrefix (\x -> x `mod` 3 == 0 && x <= e) where - val = q /~ u + val = abs q /~ u e = Prelude.floor $ Prelude.logBase 10 val :: Prelude.Int -- Selects the first prefix in the list of prefix candidates whose scale exponent matches the supplied predicate. From badc738e1df9697fde947ccff549761c4ee92200 Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Mon, 25 Jul 2016 21:06:40 -0400 Subject: [PATCH 06/11] Removed redundant import. --- src/Numeric/Units/Dimensional/SIUnits.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Numeric/Units/Dimensional/SIUnits.hs b/src/Numeric/Units/Dimensional/SIUnits.hs index 392372cc..02ca8887 100644 --- a/src/Numeric/Units/Dimensional/SIUnits.hs +++ b/src/Numeric/Units/Dimensional/SIUnits.hs @@ -61,7 +61,6 @@ module Numeric.Units.Dimensional.SIUnits ) where -import Control.Monad (join) import Data.List (sortBy, find) import Data.Maybe (maybe) import Data.Ord (comparing, Down(..)) From ea6d3fe5ab07a89a0fb1336163986d144b97da28 Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Sun, 7 Aug 2016 14:28:13 -0400 Subject: [PATCH 07/11] Changed Prefix so that emptyPrefix can be a Prefix. --- src/Numeric/Units/Dimensional/SIUnits.hs | 27 ++++++++----------- .../Units/Dimensional/UnitNames/Internal.hs | 18 ++++++++----- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Numeric/Units/Dimensional/SIUnits.hs b/src/Numeric/Units/Dimensional/SIUnits.hs index 02ca8887..ea87b86c 100644 --- a/src/Numeric/Units/Dimensional/SIUnits.hs +++ b/src/Numeric/Units/Dimensional/SIUnits.hs @@ -57,7 +57,7 @@ module Numeric.Units.Dimensional.SIUnits -- $submultiples deci, centi, milli, micro, nano, pico, femto, atto, zepto, yocto, -- $reified-prefixes - Prefix, applyPrefix, applyOptionalPrefix, siPrefixes, appropriatePrefix, withAppropriatePrefix, appropriatePrefix', withAppropriatePrefix' + Prefix, applyPrefix, siPrefixes, appropriatePrefix, withAppropriatePrefix, appropriatePrefix', withAppropriatePrefix' ) where @@ -72,7 +72,7 @@ import qualified Numeric.Units.Dimensional.UnitNames as N import Numeric.Units.Dimensional.UnitNames.Internal (ucum, ucumMetric) import qualified Numeric.Units.Dimensional.UnitNames.Internal as I import Numeric.NumType.DK.Integers ( pos3 ) -import Prelude ( Eq(..), ($), (.), Num, Fractional, Floating, RealFrac(..), Maybe(..), otherwise, error, Ord(..), fst, snd, Int, Bool, fmap, mod, (&&)) +import Prelude ( Eq(..), ($), (.), Num, Fractional, Floating, RealFrac(..), id, otherwise, error, Ord(..), Int, Bool, mod, (&&)) import qualified Prelude {- $multiples @@ -119,11 +119,6 @@ applyPrefix p u = mkUnitQ n' x u n' = N.applyPrefix p (name u) x = N.scaleFactor p --- | Applies an optional 'Prefix' to a 'Metric' 'Unit', creating a 'NonMetric' unit. -applyOptionalPrefix :: (Fractional a) => Maybe Prefix -> Unit 'Metric d a -> Unit 'NonMetric d a -applyOptionalPrefix Nothing = weaken -applyOptionalPrefix (Just p) = applyPrefix p - deci, centi, milli, micro, nano, pico, femto, atto, zepto, yocto :: Fractional a => Unit 'Metric d a -> Unit 'NonMetric d a deci = applyPrefix I.deci @@ -152,10 +147,10 @@ list of all prefixes defined by the SI. -- -- Note that the supplied prefix need not be 'Metric'. This is intended for use to compute a prefix to insert -- somewhere in the denominator of a composite (and hence 'NonMetric') unit. -appropriatePrefix :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Maybe Prefix +appropriatePrefix :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Prefix appropriatePrefix u q = selectPrefix (<= e) where - val = q /~ u + val = abs q /~ u e = Prelude.floor $ Prelude.logBase 10 val :: Prelude.Int -- | Selects the appropriate 'Prefix' to use with a 'Metric' unit when using it to display @@ -167,19 +162,19 @@ appropriatePrefix u q = selectPrefix (<= e) -- -- Note that the supplied prefix need not be 'Metric'. This is intended for use to compute a prefix to insert -- somewhere in the denominator of a composite (and hence 'NonMetric') unit. -appropriatePrefix' :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Maybe Prefix +appropriatePrefix' :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Prefix appropriatePrefix' u q = selectPrefix (\x -> x `mod` 3 == 0 && x <= e) where val = abs q /~ u e = Prelude.floor $ Prelude.logBase 10 val :: Prelude.Int -- Selects the first prefix in the list of prefix candidates whose scale exponent matches the supplied predicate. -selectPrefix :: (Int -> Bool) -> Maybe Prefix -selectPrefix p = maybe (Just . Prelude.head $ siPrefixes) snd $ find (p . fst) prefixCandidates +selectPrefix :: (Int -> Bool) -> Prefix +selectPrefix p = maybe (Prelude.head siPrefixes) id $ find (p . scaleExponent) prefixCandidates -- This is a list of candidate prefixes and the least scale exponent at which each applies. -prefixCandidates :: [(Int, Maybe Prefix)] -prefixCandidates = sortBy (comparing $ Down . fst) $ (0, Nothing) : fmap (\x -> (scaleExponent x, Just x)) siPrefixes +prefixCandidates :: [Prefix] +prefixCandidates = sortBy (comparing $ Down . scaleExponent) siPrefixes -- | Constructs a version of a 'Metric' unit, by possibly applying a 'Prefix' to it, appropriate -- for display of a particular 'Quantity'. @@ -187,7 +182,7 @@ prefixCandidates = sortBy (comparing $ Down . fst) $ (0, Nothing) : fmap (\x -> -- The appropriate prefix is defined to be the largest prefix such that the resulting value -- of the quantity, expressed in the prefixed unit, is greater than or equal to one. withAppropriatePrefix :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Unit 'NonMetric d a -withAppropriatePrefix u q = applyOptionalPrefix (appropriatePrefix u q) u +withAppropriatePrefix u q = applyPrefix (appropriatePrefix u q) u -- | Constructs a version of a 'Metric' unit, by possibly applying a 'Prefix' to it, appropriate -- for display of a particular 'Quantity'. @@ -196,7 +191,7 @@ withAppropriatePrefix u q = applyOptionalPrefix (appropriatePrefix u q) u -- of the quantity, expressed in the prefixed unit, is greater than or equal to one. Only those prefixes -- whose 'scaleExponent' is a multiple of @3@ are considered. withAppropriatePrefix' :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Unit 'NonMetric d a -withAppropriatePrefix' u q = applyOptionalPrefix (appropriatePrefix' u q) u +withAppropriatePrefix' u q = applyPrefix (appropriatePrefix' u q) u {- $base-units These are the base units from section 4.1. To avoid a diff --git a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs index 0128d4fa..f0a7a996 100644 --- a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs +++ b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs @@ -139,7 +139,7 @@ type PrefixName = NameAtom 'PrefixAtom data Prefix = Prefix { -- | The name of a metric prefix. - prefixName :: PrefixName, + prefixName :: Maybe PrefixName, -- | The scale factor denoted by a metric prefix. scaleExponent :: Int } @@ -150,9 +150,6 @@ instance Ord Prefix where instance NFData Prefix where -- instance is derived from Generic instance -instance HasInterchangeName Prefix where - interchangeName = interchangeName . prefixName - -- | The name of the unit of dimensionless values. nOne :: UnitName 'NonMetric nOne = One @@ -189,6 +186,11 @@ baseUnitName d = let powers = asList $ dimension d baseUnitNames :: [UnitName 'NonMetric] baseUnitNames = [weaken nMeter, nKilogram, weaken nSecond, weaken nAmpere, weaken nKelvin, weaken nMole, weaken nCandela] +-- | This is the SI 'Prefix' that is no prefix at all, and that consequently doesn't alter the value of the base unit to +-- which it is applied. +emptyPrefix :: Prefix +emptyPrefix = Prefix Nothing 0 + deka, hecto, kilo, mega, giga, tera, peta, exa, zetta, yotta :: Prefix deka = prefix "da" "da" "deka" 1 hecto = prefix "h" "h" "hecto" 2 @@ -214,11 +216,13 @@ yocto = prefix "y" "y" "yocto" $ -24 -- | A list of all 'Prefix'es defined by the SI. siPrefixes :: [Prefix] -siPrefixes = [yocto, zepto, atto, femto, pico, nano, micro, milli, centi, deci, deka, hecto, kilo, mega, giga, tera, peta, exa, zetta, yotta] +siPrefixes = [yocto, zepto, atto, femto, pico, nano, micro, milli, centi, deci, emptyPrefix, deka, hecto, kilo, mega, giga, tera, peta, exa, zetta, yotta] -- | Forms a 'UnitName' from a 'Metric' name by applying a metric prefix. applyPrefix :: Prefix -> UnitName 'Metric -> UnitName 'NonMetric -applyPrefix = Prefixed . prefixName +applyPrefix p = case prefixName p of + Just n -> Prefixed n + Nothing -> Weaken {- We will reuse the operators and function names from the Prelude. @@ -322,7 +326,7 @@ instance HasInterchangeName (UnitName m) where prefix :: String -> String -> String -> Int -> Prefix prefix i a f q = Prefix n q where - n = NameAtom (InterchangeName i UCUM True) a f + n = Just $ NameAtom (InterchangeName i UCUM True) a f ucumMetric :: String -> String -> String -> UnitName 'Metric ucumMetric i a f = MetricAtomic $ NameAtom (InterchangeName i UCUM True) a f From 23635e32462d1e7c3023e2eca512ed9c57d06522 Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Sun, 7 Aug 2016 15:24:11 -0400 Subject: [PATCH 08/11] Implemented PrefixSets to allow more choice in dynamic prefix selection. --- src/Numeric/Units/Dimensional/SIUnits.hs | 42 +++++++------------ src/Numeric/Units/Dimensional/UnitNames.hs | 4 +- .../Units/Dimensional/UnitNames/Internal.hs | 32 ++++++++++++-- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/Numeric/Units/Dimensional/SIUnits.hs b/src/Numeric/Units/Dimensional/SIUnits.hs index ea87b86c..e4236960 100644 --- a/src/Numeric/Units/Dimensional/SIUnits.hs +++ b/src/Numeric/Units/Dimensional/SIUnits.hs @@ -61,18 +61,15 @@ module Numeric.Units.Dimensional.SIUnits ) where -import Data.List (sortBy, find) -import Data.Maybe (maybe) -import Data.Ord (comparing, Down(..)) import Data.Ratio import Numeric.Units.Dimensional import Numeric.Units.Dimensional.Quantities -import Numeric.Units.Dimensional.UnitNames (Prefix, siPrefixes, scaleExponent) +import Numeric.Units.Dimensional.UnitNames (Prefix, PrefixSet, siPrefixes, selectPrefix) import qualified Numeric.Units.Dimensional.UnitNames as N import Numeric.Units.Dimensional.UnitNames.Internal (ucum, ucumMetric) import qualified Numeric.Units.Dimensional.UnitNames.Internal as I import Numeric.NumType.DK.Integers ( pos3 ) -import Prelude ( Eq(..), ($), (.), Num, Fractional, Floating, RealFrac(..), id, otherwise, error, Ord(..), Int, Bool, mod, (&&)) +import Prelude ( Eq(..), ($), Num, Fractional, Floating, RealFrac(..), otherwise, error) import qualified Prelude {- $multiples @@ -142,56 +139,45 @@ list of all prefixes defined by the SI. -- | Selects the appropriate 'Prefix' to use with a 'Metric' unit when using it to display -- a particular 'Quantity', or 'Nothing' if the supplied unit should be used without a prefix. -- --- The appropriate prefix is defined to be the largest prefix such that the resulting value +-- The appropriate prefix is defined to be the largest SI prefix such that the resulting value -- of the quantity, expressed in the prefixed unit, is greater than or equal to one. -- --- Note that the supplied prefix need not be 'Metric'. This is intended for use to compute a prefix to insert +-- Note that the supplied unit need not be 'Metric'. This is intended for use to compute a prefix to insert -- somewhere in the denominator of a composite (and hence 'NonMetric') unit. appropriatePrefix :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Prefix -appropriatePrefix u q = selectPrefix (<= e) - where - val = abs q /~ u - e = Prelude.floor $ Prelude.logBase 10 val :: Prelude.Int +appropriatePrefix = appropriatePrefix' siPrefixes -- | Selects the appropriate 'Prefix' to use with a 'Metric' unit when using it to display -- a particular 'Quantity', or 'Nothing' if the supplied unit should be used without a prefix. -- --- The appropriate prefix is defined to be the largest prefix such that the resulting value +-- The appropriate prefix is defined to be the largest prefix in the supplied 'PrefixSet' such that the resulting value -- of the quantity, expressed in the prefixed unit, is greater than or equal to one. Only those prefixes -- whose 'scaleExponent' is a multiple of @3@ are considered. -- --- Note that the supplied prefix need not be 'Metric'. This is intended for use to compute a prefix to insert +-- Note that the supplied unit need not be 'Metric'. This is intended for use to compute a prefix to insert -- somewhere in the denominator of a composite (and hence 'NonMetric') unit. -appropriatePrefix' :: (Floating a, RealFrac a) => Unit m d a -> Quantity d a -> Prefix -appropriatePrefix' u q = selectPrefix (\x -> x `mod` 3 == 0 && x <= e) +appropriatePrefix' :: (Floating a, RealFrac a) => PrefixSet -> Unit m d a -> Quantity d a -> Prefix +appropriatePrefix' ps u q = selectPrefix ps e where val = abs q /~ u e = Prelude.floor $ Prelude.logBase 10 val :: Prelude.Int --- Selects the first prefix in the list of prefix candidates whose scale exponent matches the supplied predicate. -selectPrefix :: (Int -> Bool) -> Prefix -selectPrefix p = maybe (Prelude.head siPrefixes) id $ find (p . scaleExponent) prefixCandidates - --- This is a list of candidate prefixes and the least scale exponent at which each applies. -prefixCandidates :: [Prefix] -prefixCandidates = sortBy (comparing $ Down . scaleExponent) siPrefixes - -- | Constructs a version of a 'Metric' unit, by possibly applying a 'Prefix' to it, appropriate -- for display of a particular 'Quantity'. -- --- The appropriate prefix is defined to be the largest prefix such that the resulting value +-- The appropriate prefix is defined to be the largest SI prefix such that the resulting value -- of the quantity, expressed in the prefixed unit, is greater than or equal to one. withAppropriatePrefix :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Unit 'NonMetric d a -withAppropriatePrefix u q = applyPrefix (appropriatePrefix u q) u +withAppropriatePrefix = withAppropriatePrefix' siPrefixes -- | Constructs a version of a 'Metric' unit, by possibly applying a 'Prefix' to it, appropriate -- for display of a particular 'Quantity'. -- --- The appropriate prefix is defined to be the largest prefix such that the resulting value +-- The appropriate prefix is defined to be the largest prefix in the supplied 'PrefixSet' such that the resulting value -- of the quantity, expressed in the prefixed unit, is greater than or equal to one. Only those prefixes -- whose 'scaleExponent' is a multiple of @3@ are considered. -withAppropriatePrefix' :: (Floating a, RealFrac a) => Unit 'Metric d a -> Quantity d a -> Unit 'NonMetric d a -withAppropriatePrefix' u q = applyPrefix (appropriatePrefix' u q) u +withAppropriatePrefix' :: (Floating a, RealFrac a) => PrefixSet -> Unit 'Metric d a -> Quantity d a -> Unit 'NonMetric d a +withAppropriatePrefix' ps u q = applyPrefix (appropriatePrefix' ps u q) u {- $base-units These are the base units from section 4.1. To avoid a diff --git a/src/Numeric/Units/Dimensional/UnitNames.hs b/src/Numeric/Units/Dimensional/UnitNames.hs index a5f894e0..de5c5e93 100644 --- a/src/Numeric/Units/Dimensional/UnitNames.hs +++ b/src/Numeric/Units/Dimensional/UnitNames.hs @@ -21,9 +21,11 @@ module Numeric.Units.Dimensional.UnitNames -- * Construction of Unit Names atom, applyPrefix, (*), (/), (^), product, reduce, grouped, -- * Standard Names - baseUnitName, siPrefixes, nOne, + baseUnitName, nOne, -- * Inspecting Prefixes prefixName, scaleExponent, scaleFactor, + -- * Sets of Prefixes + PrefixSet, prefixSet, unPrefixSet, selectPrefix, siPrefixes, majorSiPrefixes, -- * Convenience Type Synonyms for Unit Name Transformations UnitNameTransformer, UnitNameTransformer2, -- * Forgetting Unwanted Phantom Types diff --git a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs index f0a7a996..044c5cf8 100644 --- a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs +++ b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs @@ -25,6 +25,9 @@ import Data.Foldable (toList) #else import Data.Foldable (Foldable, toList) #endif +import Data.Function (on) +import Data.List (sortBy, nubBy, find) +import Data.Maybe (fromMaybe) import Data.Ord import GHC.Generics hiding (Prefix) import Numeric.Units.Dimensional.Dimensions.TermLevel (Dimension', asList, HasDimension(..)) @@ -214,9 +217,32 @@ atto = prefix "a" "a" "atto" $ -18 zepto = prefix "z" "z" "zepto" $ -21 yocto = prefix "y" "y" "yocto" $ -24 --- | A list of all 'Prefix'es defined by the SI. -siPrefixes :: [Prefix] -siPrefixes = [yocto, zepto, atto, femto, pico, nano, micro, milli, centi, deci, emptyPrefix, deka, hecto, kilo, mega, giga, tera, peta, exa, zetta, yotta] +-- | A set of 'Prefix'es which necessarily includes the 'emptyPrefix'. +newtype PrefixSet = PrefixSet { unPrefixSet :: [Prefix] } + deriving (Eq, Data) + +-- | Constructs a 'PrefixSet' from a list of 'Prefix'es by ensuring that the 'emptyPrefix' is present, +-- removing duplicates, and sorting the prefixes. +prefixSet :: [Prefix] -> PrefixSet +prefixSet = PrefixSet . sortBy (comparing $ Down . scaleExponent) . nubBy ((==) `on` scaleExponent) . (emptyPrefix :) + +-- | Chooses a 'Prefix' from a 'PrefixSet', given a scale exponent. The resulting prefix will be that in the prefix set +-- whose 'scaleExponent' is least, while still greater than the supplied scale exponent. If no prefix in the set has a +-- 'scaleExponent' greater than the supplied scale exponent, then the member with the least 'scaleExponent' will be returned. +selectPrefix :: PrefixSet -> Int -> Prefix +selectPrefix ps e = fromMaybe (Prelude.head ps') $ find ((<= e) . scaleExponent) ps' + where + ps' = unPrefixSet ps + +-- | The set of all 'Prefix'es defined by the SI. +siPrefixes :: PrefixSet +siPrefixes = prefixSet [yocto, zepto, atto, femto, pico, nano, micro, milli, centi, deci, deka, hecto, kilo, mega, giga, tera, peta, exa, zetta, yotta] + +-- | The set of all major 'Prefix'es defined by the SI. +-- +-- A major prefix is one whose scale exponent is a multiple of three. +majorSiPrefixes :: PrefixSet +majorSiPrefixes = prefixSet [yocto, zepto, atto, femto, pico, nano, micro, milli, kilo, mega, giga, tera, peta, exa, zetta, yotta] -- | Forms a 'UnitName' from a 'Metric' name by applying a metric prefix. applyPrefix :: Prefix -> UnitName 'Metric -> UnitName 'NonMetric From 5a2f4437ed23955ee5face8c266b4cf0d16fbecf Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Sun, 7 Aug 2016 16:58:45 -0400 Subject: [PATCH 09/11] Added filtering of PrefixSets. Added explicit Typeable instance for old versions of GHC. --- src/Numeric/Units/Dimensional/UnitNames.hs | 2 +- src/Numeric/Units/Dimensional/UnitNames/Internal.hs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Numeric/Units/Dimensional/UnitNames.hs b/src/Numeric/Units/Dimensional/UnitNames.hs index de5c5e93..a7d7b671 100644 --- a/src/Numeric/Units/Dimensional/UnitNames.hs +++ b/src/Numeric/Units/Dimensional/UnitNames.hs @@ -25,7 +25,7 @@ module Numeric.Units.Dimensional.UnitNames -- * Inspecting Prefixes prefixName, scaleExponent, scaleFactor, -- * Sets of Prefixes - PrefixSet, prefixSet, unPrefixSet, selectPrefix, siPrefixes, majorSiPrefixes, + PrefixSet, prefixSet, unPrefixSet, filterPrefixSet, selectPrefix, siPrefixes, majorSiPrefixes, -- * Convenience Type Synonyms for Unit Name Transformations UnitNameTransformer, UnitNameTransformer2, -- * Forgetting Unwanted Phantom Types diff --git a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs index 044c5cf8..7aa52124 100644 --- a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs +++ b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs @@ -219,13 +219,19 @@ yocto = prefix "y" "y" "yocto" $ -24 -- | A set of 'Prefix'es which necessarily includes the 'emptyPrefix'. newtype PrefixSet = PrefixSet { unPrefixSet :: [Prefix] } - deriving (Eq, Data) + deriving (Eq, Data, Typeable) -- | Constructs a 'PrefixSet' from a list of 'Prefix'es by ensuring that the 'emptyPrefix' is present, -- removing duplicates, and sorting the prefixes. prefixSet :: [Prefix] -> PrefixSet prefixSet = PrefixSet . sortBy (comparing $ Down . scaleExponent) . nubBy ((==) `on` scaleExponent) . (emptyPrefix :) +-- | Filters a 'PrefixSet', retaining only those 'Prefix'es which match a supplied predicate. +-- +-- The 'emptyPrefix' is always retained, as it must be a member of every 'PrefixSet'. +filterPrefixSet :: (Prefix -> Bool) -> PrefixSet -> PrefixSet +filterPrefixSet p = prefixSet . filter p . unPrefixSet + -- | Chooses a 'Prefix' from a 'PrefixSet', given a scale exponent. The resulting prefix will be that in the prefix set -- whose 'scaleExponent' is least, while still greater than the supplied scale exponent. If no prefix in the set has a -- 'scaleExponent' greater than the supplied scale exponent, then the member with the least 'scaleExponent' will be returned. @@ -242,7 +248,7 @@ siPrefixes = prefixSet [yocto, zepto, atto, femto, pico, nano, micro, milli, cen -- -- A major prefix is one whose scale exponent is a multiple of three. majorSiPrefixes :: PrefixSet -majorSiPrefixes = prefixSet [yocto, zepto, atto, femto, pico, nano, micro, milli, kilo, mega, giga, tera, peta, exa, zetta, yotta] +majorSiPrefixes = filterPrefixSet ((== 0) . (`mod` 3) . scaleExponent) siPrefixes -- | Forms a 'UnitName' from a 'Metric' name by applying a metric prefix. applyPrefix :: Prefix -> UnitName 'Metric -> UnitName 'NonMetric From 87ee280b1c0bdf90f395d27f41c644d45a4ccf61 Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Fri, 7 Oct 2016 15:42:44 -0400 Subject: [PATCH 10/11] Added equality for units. --- CHANGELOG.md | 1 + src/Numeric/Units/Dimensional/Internal.hs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddea373c..b35ef72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ vNext from RealFloat and IEEE for inspecting floating point quantities. * Added an `AEq` instance for `Quantity`. * Added `Eq1` and `Ord1` instances for `Quantity`. +* Added `Eq` and `Eq1` instances for `Unit`. * Exposed the name of an 'AnyUnit' without promoting it to a 'Unit' first. * Exposed a way to convert atomic 'UnitName's back into 'NameAtom's. * Added dynamic selection of metric prefixes based on the magnitude of a quantity to be displayed. diff --git a/src/Numeric/Units/Dimensional/Internal.hs b/src/Numeric/Units/Dimensional/Internal.hs index a3ea17ef..20b6214c 100644 --- a/src/Numeric/Units/Dimensional/Internal.hs +++ b/src/Numeric/Units/Dimensional/Internal.hs @@ -50,8 +50,8 @@ import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed.Base as U import Prelude ( Show, Eq(..), Ord, Bounded(..), Num, Fractional, Functor, Real(..) - , String, Maybe(..), Double - , (.), ($), (++), (+), (/) + , String, Maybe(..), Double, Bool(..) + , (.), ($), (++), (+), (/), (&&) , show, otherwise, undefined, error, fmap, realToFrac ) import qualified Prelude as P @@ -131,6 +131,18 @@ instance Ord1 (SQuantity s d) where liftCompare = coerce #endif +instance (Eq a) => Eq (Unit m d a) where + (==) = areEqualUnitsBy (==) + +#if MIN_VERSION_base(4,9,0) +instance Eq1 (Unit m d) where + liftEq = areEqualUnitsBy +#endif + +-- define this here so that it is usable even when we are not conditionally compiling a Eq1 instance to define the Eq instance +areEqualUnitsBy :: (a -> b -> Bool) -> Unit m d a -> Unit m d b -> Bool +areEqualUnitsBy f (Unit n1 e1 x1) (Unit n2 e2 x2) = n1 == n2 && areExactlyEqual e1 e2 && f x1 x2 + instance HasInterchangeName (Unit m d a) where interchangeName (Unit n _ _) = interchangeName n From 925819d524ec0e7cc420618069ec927603582215 Mon Sep 17 00:00:00 2001 From: Douglas McClean Date: Fri, 7 Oct 2016 15:43:06 -0400 Subject: [PATCH 11/11] Added tests for appropriate prefix selection. --- .../Units/Dimensional/UnitNames/Internal.hs | 9 ++++++--- tests/Numeric/Units/Dimensional/SIUnitsSpec.hs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 tests/Numeric/Units/Dimensional/SIUnitsSpec.hs diff --git a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs index 7aa52124..f0b883e0 100644 --- a/src/Numeric/Units/Dimensional/UnitNames/Internal.hs +++ b/src/Numeric/Units/Dimensional/UnitNames/Internal.hs @@ -26,8 +26,7 @@ import Data.Foldable (toList) import Data.Foldable (Foldable, toList) #endif import Data.Function (on) -import Data.List (sortBy, nubBy, find) -import Data.Maybe (fromMaybe) +import Data.List (sortBy, nubBy) import Data.Ord import GHC.Generics hiding (Prefix) import Numeric.Units.Dimensional.Dimensions.TermLevel (Dimension', asList, HasDimension(..)) @@ -236,8 +235,12 @@ filterPrefixSet p = prefixSet . filter p . unPrefixSet -- whose 'scaleExponent' is least, while still greater than the supplied scale exponent. If no prefix in the set has a -- 'scaleExponent' greater than the supplied scale exponent, then the member with the least 'scaleExponent' will be returned. selectPrefix :: PrefixSet -> Int -> Prefix -selectPrefix ps e = fromMaybe (Prelude.head ps') $ find ((<= e) . scaleExponent) ps' +selectPrefix ps e = go ((<= e) . scaleExponent) ps' where + go _ (x:[]) = x + go f (x:xs) | f x = x + | otherwise = go f xs + go _ _ = emptyPrefix ps' = unPrefixSet ps -- | The set of all 'Prefix'es defined by the SI. diff --git a/tests/Numeric/Units/Dimensional/SIUnitsSpec.hs b/tests/Numeric/Units/Dimensional/SIUnitsSpec.hs new file mode 100644 index 00000000..90ac7376 --- /dev/null +++ b/tests/Numeric/Units/Dimensional/SIUnitsSpec.hs @@ -0,0 +1,18 @@ +module Numeric.Units.Dimensional.SIUnitsSpec where + +import Numeric.Units.Dimensional.Prelude +import Test.Hspec + +spec :: Spec +spec = do + describe "Dynamic prefix selection" $ do + it "selects no prefix when appropriate" $ do + withAppropriatePrefix meter ((1.3 :: Double) *~ meter) `shouldBe` weaken meter + it "selects kilo as a prefix when appropriate" $ do + withAppropriatePrefix newton ((-1742.1 :: Double) *~ newton) `shouldBe` kilo newton + it "selects yotta as a prefix when appropriate" $ do + withAppropriatePrefix gram ((875 :: Double) *~ yotta gram) `shouldBe` yotta gram + it "selects atto as a prefix when appropriate" $ do + withAppropriatePrefix second ((85.4 :: Double) *~ atto second) `shouldBe` atto second + it "selects yocto as a prefix when appropriate" $ do + withAppropriatePrefix watt ((1e-7 :: Double) *~ yocto watt) `shouldBe` yocto watt