-- Copyright (c) 2016-present, Facebook, Inc. -- All rights reserved. -- -- This source code is licensed under the BSD-style license found in the -- LICENSE file in the root directory of this source tree. -- | This module shadows names of some units previously defined in -- 'Duckling.Distance.Types'. -- Therefore, the units from that module must be used qualified by module. module Duckling.DistanceUnits.Types ( ContextualDistance (..) , toSystemUnit , toRawUnit ) where import Data.Semigroup import Data.Tuple.Extra (both) import Prelude import qualified Duckling.Distance.Types as TDist -- | Supports deferred resolution of ambiguous units. -- Note that this sum type cannot be simply replaced by -- "Maybe (ContextualDistance Double DeferrableUnit)" -- (with "Nothing" representing "Nonrelatable"). -- See "NOTE A" below. data ContextualDistance = Nonrelatable -- ^ If two different ambiguous units were to be composed, then -- it would not be possible to decide which to resolve to. As of today, -- this won't happen since for now "M" is the lone ambiguous unit. | ContextualDistance Double DeferrableUnit -- | This represents Units that have not yet been established as unrelatable. -- The "Definite" constructor is purposely listed in front of "Ambiguous", -- so that it will be preferred when taking choosing the "min" between -- an "Ambiguous" and "Definite" unit. data DeferrableUnit = Definite SystemUnit | Ambiguous AmbiguousUnit deriving (Eq, Ord) -- | These measurement-system-specific units exist to disambiguate -- the \"M\" of the 'Unit' type and to maintain the measurement-system context -- for resolving to the appropriate precision. -- -- It's also just handy to have separate programmatically accessible lists -- of units for each type of measurement system. data SystemUnit = Metric MetricUnit | Imperial ImperialUnit deriving (Eq, Ord) data ImperialUnit = Inch | Foot | Yard | Mile deriving (Eq, Ord) data MetricUnit = Millimetre | Centimetre | Metre | Kilometre deriving (Eq, Ord) -- | Currently there is only one actually ambiguous unit, but this design allows -- for expansion. data AmbiguousUnit = M -- ^ "Miles" or "Metres" deriving (Eq, Ord) -- | Represents a value with an unambiguous unit data UnitValuePair = UnitValuePair SystemUnit Double sumScaledUnits :: SystemUnit -> (UnitValuePair, UnitValuePair) -> Double sumScaledUnits preferredUnit = uncurry (+) . both (scaleUnits preferredUnit) -- | Determine the definite meaning of an "Ambiguous" unit using the context of -- another "Definite" unit reconcileAmbiguousWithDefinite :: Double -> AmbiguousUnit -> Double -> SystemUnit -> ContextualDistance reconcileAmbiguousWithDefinite av au dv du = ContextualDistance combinedValue $ Definite preferredUnit where resolvedAmbiguousUnit = resolveUnit au du preferredUnit = du `min` resolvedAmbiguousUnit combinedValue = sumScaledUnits preferredUnit (UnitValuePair resolvedAmbiguousUnit av, UnitValuePair du dv) -- | When both Metric and Imperial units are given, resolve to Metric. -- Otherwise, preserve the original measurement system and use the smaller unit. -- For the purpose of this resolution, all Metric units are considered "smaller" -- than Imperial units. instance Semigroup ContextualDistance where _ <> Nonrelatable = Nonrelatable Nonrelatable <> _ = Nonrelatable (ContextualDistance v1 u@(Ambiguous u1)) <> (ContextualDistance v2 (Ambiguous u2)) | u1 == u2 = ContextualDistance (v1 + v2) u | otherwise = Nonrelatable -- NOTE A: Needing to return "Nonrelatable" in this edge case is why -- the two-member "ContextualDistance" sum type cannot be simply -- represented as (Maybe (ContextualDistance Double DeferrableUnit)). (ContextualDistance av (Ambiguous au)) <> (ContextualDistance dv (Definite du)) = reconcileAmbiguousWithDefinite av au dv du (ContextualDistance dv (Definite du)) <> (ContextualDistance av (Ambiguous au)) = reconcileAmbiguousWithDefinite av au dv du (ContextualDistance v1 (Definite u1)) <> (ContextualDistance v2 (Definite u2)) = ContextualDistance v $ Definite u where v = sumScaledUnits u (UnitValuePair u1 v1, UnitValuePair u2 v2) u = u1 `min` u2 resolveUnit :: AmbiguousUnit -> SystemUnit -> SystemUnit resolveUnit M (Metric _) = Metric Metre resolveUnit M (Imperial _) = Imperial Mile -- | Disambiguation of original Unit type toSystemUnit :: TDist.Unit -> DeferrableUnit toSystemUnit TDist.M = Ambiguous M toSystemUnit TDist.Millimetre = Definite $ Metric Millimetre toSystemUnit TDist.Centimetre = Definite $ Metric Centimetre toSystemUnit TDist.Metre = Definite $ Metric Metre toSystemUnit TDist.Kilometre = Definite $ Metric Kilometre toSystemUnit TDist.Inch = Definite $ Imperial Inch toSystemUnit TDist.Foot = Definite $ Imperial Foot toSystemUnit TDist.Yard = Definite $ Imperial Yard toSystemUnit TDist.Mile = Definite $ Imperial Mile -- | Reconversion to original Unit type toRawUnit :: DeferrableUnit -> TDist.Unit toRawUnit (Ambiguous M) = TDist.M toRawUnit (Definite (Metric Millimetre)) = TDist.Millimetre toRawUnit (Definite (Metric Centimetre)) = TDist.Centimetre toRawUnit (Definite (Metric Metre)) = TDist.Metre toRawUnit (Definite (Metric Kilometre)) = TDist.Kilometre toRawUnit (Definite (Imperial Inch)) = TDist.Inch toRawUnit (Definite (Imperial Foot)) = TDist.Foot toRawUnit (Definite (Imperial Yard)) = TDist.Yard toRawUnit (Definite (Imperial Mile)) = TDist.Mile -- | Convert a distance to the given units. -- This only works if the unit is unambiguous. scaleUnits :: SystemUnit -- ^ target unit -> UnitValuePair -- ^ Original unit and value -> Double scaleUnits targetUnit (UnitValuePair startingUnit v) | startingUnit == targetUnit = v | otherwise = inSIUnits v startingUnit / inSIUnits 1 targetUnit -- | This is used when distances -- must be normalized across measurement systems. -- The Metric metre is the Standard International unit of distance. inSIUnits :: Double -> SystemUnit -> Double inSIUnits val (Metric u) = inMetres u val inSIUnits val (Imperial u) = inInches u val * metersPerInch -- | This conversion factor is exact. metersPerInch :: Double metersPerInch = 0.0254 inMetres :: Fractional a => MetricUnit -> a -> a inMetres Millimetre n = n / 1000 inMetres Centimetre n = n / 100 inMetres Metre n = n inMetres Kilometre n = n * 1000 inInches :: Num a => ImperialUnit -> a -> a inInches Inch n = n inInches Foot n = 12 * n inInches Yard n = 3 * inInches Foot n inInches Mile n = 5280 * inInches Foot n