This is an alternative site for discovering Elm packages. You may be looking for the official Elm package site instead.
1.0.0 1.0.1
README

CssMath

Arithmetic and unit conversions with CSS values

This library extends CssBasics by providing helpers for dealing with numeric components of CssValue types. It allows you to add, subtract, scale, and calculate ratios of numeric CSS values, to convert between units and numbers, and to convert among absolute and relative units of length.

See here for CSS unit specifications.

Basic Numeric Operations

add : CssValue -> CssValue -> Result String CssValue

Add the value in the first argument to the value(s) in the second argument and return the result, or return an error message if one or more of the values is of an incompatible type.

Type compatibility: You can add two Num values, two Unit values with the same unit, or two Unit values with different units that are both absolute (Px, In, Cm, Mm, Pt, Pc). You can also add a single Num or Unit value to a Sides or Multiple, so long as all of the values are compatible. See the examples below.

Zero values are an exception: Num 0 or Unit 0 _ (with any unit type) may be added to a non-zero unit value without producing an error.

Num 1
  |> add (Num 1)

--> Ok (Num 2)

Unit 0.5 In
  |> add (Unit 36 Pt)

--> Ok (Unit 92 Px)

Unit 1 Em
  |> add (Unit 0.5 Em)

--> Ok (Unit 1.5 Em)

Unit 12 Px
  |> add (Num 1)

--> Err ".."

Unit 12 Px
  |> add (Unit 1 Em)

--> Err ".."

Sides [Unit 0.5 In, Unit 36 Pt, Unit 0 NoUnit, Unit 0 Em]
  |> add (Unit 0.5 In)

--> Ok (Sides [Unit 1 In, Unit 92 Px, Unit 46 Px, Unit 46 Px])

Unit 12 Px
  |> add (Num 0)

--> Ok (Unit 12 Px)

Unit 12 Px
  |> add (Num 1)

--> Err ".."
subtract : CssValue -> CssValue -> Result String CssValue

Subtract the value in the first argument from the value(s) in the second argument and return the result, or return an error message if one or more of the values is of an incompatible type. For details on type compatibility, see the documentation for add.

Unit 1 In
  |> subtract (Unit 36 Pt)

--> Ok (Unit 46 Px)
scale : Float -> CssValue -> Result String CssValue

Scale the value(s) in the second argument by the factor given as the first argument, or return an error message if one or more of the values is of an incompatible type.

The argument may be a Sides or Multiple value, which will return a Sides or Multiple containing the scaled unit values. If a Multiple contains both numeric and non-numeric values, the numeric portion may be scaled using this function, which will leave the non-numeric values unchanged. This behavior is different from add and subtract, which will return an error if the arguments contain any non-numeric values.

Unit 2 Em
  |> scale 0.5

--> Ok (Unit 1 Em)

Sides [Unit 12 Px, Unit 2 Em]
  |> scale 0.5

--> Ok (Sides [Unit 6 Px, Unit 1 Em])

Multiple " " [Unit 1 Px, Str "dashed", Col Color.red]
  |> scale 2

--> Ok (Multiple " " [Unit 2 Px, Str "dashed", Col (RGBA 204 0 0 1)])
ratio : (CssValue, CssValue) -> Result String Float

Given a tuple of Num or Unit values, calculate the ratio of the first value to the second, or return an error message if one or more of the values is of an incompatible type.

Type compatibility works the same as for add and subtract, but Sides and Multiple values are not allowed.

(Unit 46 Px, Unit 1 In)
  |> ratio

--> Ok 0.5

(Num 0, Unit 1 In)
  |> ratio

--> Ok 0

Applying Custom Operations

numOp : (Float -> Float -> Float) -> (CssValue, CssValue) -> Result String CssValue

Apply a numeric operation to a pair of CssValue values. This is the generic function called by add and subtract, so see the documentation above for details on type compatibility.

Unit Conversions

absToPx : CssValue -> Result String CssValue

Convert any value in absolute units (Px, In, Cm, Mm, Pt, Pc) to a pixel (Px) value. Returns an error if the argument is a non-zero value with a relative unit, or if the argument contains one or more non-unit values.

The argument may be a Sides or Multiple value, which will return a Sides or Multiple with the converted unit values. Num 0 will convert to Unit 0 Px, but non-zero Num values will return an error message.

Unit 1 In
  |> absToPx

--> Ok (Unit 92 Px)

Unit 1 Em
  |> absToPx

--> Err ".."

Sides [Unit 0.5 In, Unit 36 Pt, Unit 0 NoUnit, Unit 0 Em]
  |> absToPx

--> Ok (Sides [Unit 46 Px, Unit 46 Px, Unit 0 Px, Unit 0 Px])

Num 0
  |> absToPx

--> Ok (Unit 0 Px)

Num 1
  |> absToPx

--> Err ".."
relToPx : Float -> CssValue -> Result String CssValue

Convert any value in relative units (Percent, Em, Ex, Ch, Rem, Vh, Vw, Vmin, Vmax) to a pixel (Px) value. The first argument supplies the pixel length to which the unit value is relative. Conversions from Ex and Ch values are approximate, as the exact values of these units are dependent on font properties.

Returns an error if the argument is a non-zero value with a relative unit, or if the argument contains one or more non-unit values.

The argument may be a Sides or Multiple value, which will return a Sides or Multiple with the converted unit values. Num 0 will convert to Unit 0 Px, but non-zero Num values will return an error message.

Unit 1 Em
  |> relToPx 16

--> Ok (Unit 16 Px)
vpRelToPx : (Float, Float) -> CssValue -> Result String CssValue

Convert any value in viewport-relative units (Vh, Vw, Vmin, Vmax) to a pixel (Px) value. The first argument supplies the width and height of the viewport in pixels as a 2-tuple.

Returns an error if the argument is a non-zero value with a non-viewport-relative unit, or if the argument contains one or more non-unit values.

The argument may be a Sides or Multiple value, which will return a Sides or Multiple with the converted unit values. Num 0 will convert to Unit 0 Px, but non-zero Num values will return an error message.

Unit 10 Vmin
  |> vpRelToPx (600, 800)

--> Ok (Unit 60 Px)
absToRem : Float -> CssValue -> Result String CssValue

Convert any value in absolute units (Px, In, Cm, Mm, Pt, Pc) to a Rem value. In CSS, "rem" is a relative unit defined by the font size of the HTML <body> element. To convert to rems, the first argument to this function must supply this base font size in pixels.

Returns an error if the argument is a non-zero value with a relative unit, or if the argument contains one or more non-unit values.

The argument may be a Sides or Multiple value, which will return a Sides or Multiple with the converted unit values. Num 0 will convert to Unit 0 Rem, but non-zero Num values will return an error message.

Unit 12 Px
  |> absToRem 16

--> Ok (Unit 0.75 Rem)

Helpers

toUnit : UnitType -> Float -> CssValue

Convenience function to convert a number to a Unit

toNumber : CssValue -> Result String Float

Extracts the numeric part of a Num or Unit value, or returns an error message if the CssValue is not a Num or Unit

toNumberList : CssValue -> Result String (List Float)

Extracts a list of numbers from a Sides or Multiple value, or a list containing a single number from a Num or Unit. Returns an error message if any of the values are non-numeric.

isNonZero : CssValue -> Result String Bool

Returns a True result if the argument is a Num or Unit containing a non-zero value or a Sides or Multiple value containing at least one non-zero value. Returns an error message if the argument contains one or more non-numeric values.

module CssMath exposing
  ( add, subtract, scale, ratio, numOp, absToPx, relToPx, vpRelToPx, absToRem
  , toNumber, toNumberList, toUnit, isNonZero
  )

{-|
## Arithmetic and unit conversions with CSS values

This library extends `CssBasics` by providing helpers for dealing with numeric
components of `CssValue` types. It allows you to add, subtract, scale, and
calculate ratios of numeric CSS values, to convert between units and numbers,
and to convert among absolute and relative units of length.

See
[here](https://developer.mozilla.org/en-US/docs/Web/CSS/length)
for CSS unit specifications.

# Basic Numeric Operations
@docs add, subtract, scale, ratio

## Applying Custom Operations
@docs numOp

# Unit Conversions
@docs absToPx, relToPx, vpRelToPx, absToRem

# Helpers
@docs toUnit, toNumber, toNumberList, isNonZero

-}

import Toolkit.Operators exposing (..)
import Toolkit.Helpers as Helpers
import CssBasics exposing (CssValue(..), UnitType(..))


-- BASIC NUMBER/UNIT OPERATIONS

{-| Add the value in the first argument to the value(s) in the second argument
and return the result, or return an error message if one or more of the
values is of an incompatible type.

**Type compatibility:**
You can add two `Num` values, two `Unit` values with the same unit, or two
`Unit` values with different units that are both absolute (`Px`, `In`, `Cm`,
`Mm`, `Pt`, `Pc`). You can also add a single `Num` or `Unit` value to a `Sides`
or `Multiple`, so long as all of the values are compatible. See the examples
below.

*Zero values are an exception:*
`Num 0` or `Unit 0 _` (with any unit type) may be added to a non-zero unit value
without producing an error.

    Num 1
      |> add (Num 1)

    --> Ok (Num 2)

    Unit 0.5 In
      |> add (Unit 36 Pt)

    --> Ok (Unit 92 Px)

    Unit 1 Em
      |> add (Unit 0.5 Em)

    --> Ok (Unit 1.5 Em)

    Unit 12 Px
      |> add (Num 1)

    --> Err ".."

    Unit 12 Px
      |> add (Unit 1 Em)

    --> Err ".."

    Sides [Unit 0.5 In, Unit 36 Pt, Unit 0 NoUnit, Unit 0 Em]
      |> add (Unit 0.5 In)

    --> Ok (Sides [Unit 1 In, Unit 92 Px, Unit 46 Px, Unit 46 Px])

    Unit 12 Px
      |> add (Num 0)

    --> Ok (Unit 12 Px)

    Unit 12 Px
      |> add (Num 1)

    --> Err ".."

-}
add : CssValue -> CssValue -> Result String CssValue
add second first =
  (first, second)
    |> numOp (+)


{-| Subtract the value in the first argument from the value(s) in the second
argument and return the result, or return an error message if one or more of the
values is of an incompatible type. For details on type compatibility, see the
documentation for `add`.

    Unit 1 In
      |> subtract (Unit 36 Pt)

    --> Ok (Unit 46 Px)
-}
subtract : CssValue -> CssValue -> Result String CssValue
subtract second first =
  (first, second)
    |> numOp (-)


{-| Scale the value(s) in the second argument by the factor given as the first
argument, or return an error message if one or more of the values is of an
incompatible type.

The argument may be a `Sides` or `Multiple` value, which will return a `Sides`
or `Multiple` containing the scaled unit values. If a `Multiple` contains both
numeric and non-numeric values, the numeric portion may be scaled using this
function, which will leave the non-numeric values unchanged. This behavior is
different from `add` and `subtract`, which will return an error if the
arguments contain any non-numeric values.

    Unit 2 Em
      |> scale 0.5

    --> Ok (Unit 1 Em)

    Sides [Unit 12 Px, Unit 2 Em]
      |> scale 0.5

    --> Ok (Sides [Unit 6 Px, Unit 1 Em])

    Multiple " " [Unit 1 Px, Str "dashed", Col Color.red]
      |> scale 2

    --> Ok (Multiple " " [Unit 2 Px, Str "dashed", Col (RGBA 204 0 0 1)])
-}
scale : Float -> CssValue -> Result String CssValue
scale factor value =
  let
    allNonNumeric values =
      values
       .|> toNumber
       .|> Result.toMaybe
        |> List.all (\v -> v == Nothing)

  in
    case value of
      Num number ->
        (number, factor)
        @@|> (*)
          |> Num
          |> Ok

      Unit number unitType ->
        (number, factor)
        @@|> (*)
          |> toUnit unitType
          |> Ok

      Sides values ->
        values
         .|> scale factor
          |> Helpers.resultList errorMsg
         !|> Sides

      Multiple separator values ->
        if values |> allNonNumeric then errorMsg |> Err
        else values |> Ok
         !|> List.map (\v -> v |> scale factor != v)
         !|> Multiple separator

      _ ->
        errorMsg
          |> Err


{-| Given a tuple of `Num` or `Unit` values, calculate the ratio of the first
value to the second, or return an error message if one or more of the values is
of an incompatible type.

Type compatibility works the same as for `add` and `subtract`, but `Sides` and
`Multiple` values are not allowed.

    (Unit 46 Px, Unit 1 In)
      |> ratio

    --> Ok 0.5

    (Num 0, Unit 1 In)
      |> ratio

    --> Ok 0
-}
ratio : (CssValue, CssValue) -> Result String Float
ratio (first, second) =
  (first, second)
    |> numOp (/)
   !+> toNumber


-- CUSTOM OPERATIONS

{-| Apply a numeric operation to a pair of `CssValue` values. This is the
generic function called by `add` and `subtract`, so see the documentation above
for details on type compatibility.
-}
numOp : (Float -> Float -> Float) -> (CssValue, CssValue) -> Result String CssValue
numOp op (first, second) =
  case (first, second) of
    (Num n1, Num n2) ->
      (n1, n2)
      @@|> op
        |> Num
        |> Ok

    (Unit n1 u1, Unit n2 u2)  ->
      if u1 == u2 then
        (n1, n2)
        @@|> op
          |> toUnit u1
          |> Ok

      else
        (first, second)
        ..|> absToPx
        ..|> Result.andThen toNumber
        @@|> Result.map2 op
         !|> toUnit Px

    (Sides values, _ ) ->
      values
       .|> flip (,) second
       .|> numOp op
        |> Helpers.resultList errorMsg
       !|> Sides

    (Multiple separator values, _ ) ->
      values
       .|> flip (,) second
       .|> numOp op
        |> Helpers.resultList errorMsg
       !|> Multiple separator

    _ ->
      (first, second)
      ..|> absToPx
      ..|> Result.andThen toNumber
      @@|> Result.map2 op
       !|> toUnit Px


-- UNIT CONVERSIONS

{-| Convert any value in absolute units (`Px`, `In`, `Cm`, `Mm`, `Pt`, `Pc`) to
a pixel (`Px`) value. Returns an error if the argument is a non-zero value with
a relative unit, or if the argument contains one or more non-unit values.

The argument may be a `Sides` or `Multiple` value, which will return a `Sides`
or `Multiple` with the converted unit values. `Num 0` will convert to
`Unit 0 Px`, but non-zero `Num` values will return an error message.

    Unit 1 In
      |> absToPx

    --> Ok (Unit 92 Px)

    Unit 1 Em
      |> absToPx

    --> Err ".."

    Sides [Unit 0.5 In, Unit 36 Pt, Unit 0 NoUnit, Unit 0 Em]
      |> absToPx

    --> Ok (Sides [Unit 46 Px, Unit 46 Px, Unit 0 Px, Unit 0 Px])

    Num 0
      |> absToPx

    --> Ok (Unit 0 Px)

    Num 1
      |> absToPx

    --> Err ".."

-}
absToPx : CssValue -> Result String CssValue
absToPx value =
  let
    convertToPx fromUnit number =
      case fromUnit of
        Px ->
          number
            |> toUnit Px
            |> Ok

        In ->
          number
            |> (*) 92
            |> toUnit Px
            |> Ok

        Cm ->
          number
            |> flip (/) 2.54
            |> convertToPx In

        Mm ->
          number
            |> flip (/) 25.4
            |> convertToPx In

        Pt ->
          number
            |> flip (/) 72
            |> convertToPx In

        Pc ->
          number
            |> (*) 12
            |> convertToPx Pt

        _ ->
          "`UnitType` must be an absolute unit: In, Cm, Mm, Pt, Pc"
            |> Err

  in
    case value of
      Num number ->
        if number == 0 then
          Unit 0 Px
            |> Ok

        else
          errorMsg
            |> Err

      Unit number unitType ->
        if number == 0 then
          Unit 0 Px
            |> Ok

        else
          number
            |> convertToPx unitType

      Sides values ->
        values
         .|> absToPx
          |> Helpers.resultList errorMsg
         !|> Sides

      Multiple separator values ->
        values
         .|> absToPx
          |> Helpers.resultList errorMsg
         !|> Multiple separator

      _ ->
        errorMsg
          |> Err


{-| Convert any value in relative units (`Percent`, `Em`, `Ex`, `Ch`, `Rem`,
`Vh`, `Vw`, `Vmin`, `Vmax`) to a pixel (`Px`) value. The first argument supplies
the pixel length to which the unit value is relative. Conversions from `Ex` and
`Ch` values are approximate, as the exact values of these units are dependent on
font properties.

Returns an error if the argument is a non-zero value with a relative unit, or if
the argument contains one or more non-unit values.

The argument may be a `Sides` or `Multiple` value, which will return a `Sides`
or `Multiple` with the converted unit values. `Num 0` will convert to
`Unit 0 Px`, but non-zero `Num` values will return an error message.

    Unit 1 Em
      |> relToPx 16

    --> Ok (Unit 16 Px)
-}
relToPx : Float -> CssValue -> Result String CssValue
relToPx base value =
  let
    multiply values =
      values
      @@|> (*)
        |> toUnit Px
        |> Ok

    convertToPx fromUnit (number, base) =
      case fromUnit of
        Percent ->
          (number / 100, base)
            |> multiply

        Em ->
          (number, base)
            |> multiply

        Ex ->
          (number, base / 2)
            |> multiply

        Ch ->
          (number, base / 2)
            |> multiply

        Rem ->
          (number, base)
            |> multiply

        Vh ->
          (number / 100, base)
            |> multiply

        Vw ->
          (number / 100, base)
            |> multiply

        Vmin ->
          (number / 100, base)
            |> multiply

        Vmax ->
          (number / 100, base)
            |> multiply

        _ ->
          "`UnitType` must be a relative unit: `Percent`, `Em`, `Ex`, `Ch`, "
            |++ "`Rem`, `Vh`, `Vw`, `Vmin`, `Vmax`"
            |> Err

  in
    case value of
      Num number ->
        if number == 0 then
          Unit 0 Px
            |> Ok

        else
          errorMsg
            |> Err

      Unit number unitType ->
        if number == 0 then
          Unit 0 Px
            |> Ok

        else
          (number, base)
            |> convertToPx unitType

      Sides values ->
        values
         .|> relToPx base
          |> Helpers.resultList errorMsg
         !|> Sides

      Multiple separator values ->
        values
         .|> relToPx base
          |> Helpers.resultList errorMsg
         !|> Multiple separator

      _ ->
        errorMsg
          |> Err


{-| Convert any value in viewport-relative units (`Vh`, `Vw`, `Vmin`, `Vmax`) to
a pixel (`Px`) value. The first argument supplies the width and height of the
viewport in pixels as a 2-tuple.

Returns an error if the argument is a non-zero value with a
non-viewport-relative unit, or if the argument contains one or more non-unit
values.

The argument may be a `Sides` or `Multiple` value, which will return a `Sides`
or `Multiple` with the converted unit values. `Num 0` will convert to
`Unit 0 Px`, but non-zero `Num` values will return an error message.

    Unit 10 Vmin
      |> vpRelToPx (600, 800)

    --> Ok (Unit 60 Px)
-}
vpRelToPx : (Float, Float) -> CssValue -> Result String CssValue
vpRelToPx (width, height) value =
  let
    multiply values =
      values
      @@|> (*)
        |> toUnit Px
        |> Ok

    convertToPx fromUnit (width, height) number =
      case fromUnit of
        Vh ->
          (number / 100, height)
            |> multiply

        Vw ->
          (number / 100, width)
            |> multiply

        Vmin ->
          (number / 100, min width height)
            |> multiply

        Vmax ->
          (number / 100, max width height)
            |> multiply

        _ ->
          "`UnitType` must be a viewport-relative unit: `Vh`, `Vw`, `Vmin`, "
            |++ "`Vmax`"
            |> Err

  in
    case value of
      Num number ->
        if number == 0 then
          Unit 0 Px
            |> Ok

        else
          errorMsg
            |> Err

      Unit number unitType ->
        if number == 0 then
          Unit 0 Px
            |> Ok

        else
          number
            |> convertToPx unitType (width, height)

      Sides values ->
        values
         .|> vpRelToPx (width, height)
          |> Helpers.resultList errorMsg
         !|> Sides

      Multiple separator values ->
        values
         .|> vpRelToPx (width, height)
          |> Helpers.resultList errorMsg
         !|> Multiple separator

      _ ->
        errorMsg
          |> Err


{-| Convert any value in absolute units (`Px`, `In`, `Cm`, `Mm`, `Pt`, `Pc`) to
a `Rem` value. In CSS, "rem" is a relative unit defined by the font size of the
HTML `<body>` element. To convert to rems, the first argument to this function
must supply this base font size in pixels.

Returns an error if the argument is a non-zero value with a relative unit, or if
the argument contains one or more non-unit values.

The argument may be a `Sides` or `Multiple` value, which will return a `Sides`
or `Multiple` with the converted unit values. `Num 0` will convert to
`Unit 0 Rem`, but non-zero `Num` values will return an error message.

    Unit 12 Px
      |> absToRem 16

    --> Ok (Unit 0.75 Rem)
-}
absToRem : Float -> CssValue -> Result String CssValue
absToRem baseFontSize value =
  case value of
    Num number ->
      if number == 0 then
        Unit 0 Px
          |> Ok

      else
        errorMsg
          |> Err

    Unit number unitType ->
      if number == 0 then
        Unit 0 Px
          |> Ok

      else
        value
          |> absToPx
         !+> toNumber
         !|> flip (/) baseFontSize
         !|> toUnit Rem

    Sides values ->
      values
       .|> absToRem baseFontSize
        |> Helpers.resultList errorMsg
       !|> Sides

    Multiple separator values ->
      values
       .|> absToRem baseFontSize
        |> Helpers.resultList errorMsg
       !|> Multiple separator

    _ ->
      errorMsg
        |> Err


-- HELPERS

{-| Convenience function to convert a number to a `Unit`
-}
toUnit : UnitType -> Float -> CssValue
toUnit unitType number =
  Unit number unitType


{-| Extracts the numeric part of a `Num` or `Unit` value, or returns an error
message if the `CssValue` is not a `Num` or `Unit`
-}
toNumber : CssValue -> Result String Float
toNumber value =
  case value of
    Num number ->
      number
        |> Ok

    Unit number unitType ->
      number
        |> Ok

    _ ->
      "Argument must be a `Num` or `Unit`"
        |> Err


{-| Extracts a list of numbers from a `Sides` or `Multiple` value, or a list
containing a single number from a `Num` or `Unit`. Returns an error message if
any of the values are non-numeric.
-}
toNumberList : CssValue -> Result String (List Float)
toNumberList value =
  case value of
    Num number ->
      number
        |> List.singleton
        |> Ok

    Unit number unitType ->
      number
        |> List.singleton
        |> Ok

    Sides list ->
      list
       .|> toNumber
        |> Helpers.resultList errorMsg

    Multiple separator list ->
      list
       .|> toNumber
        |> Helpers.resultList errorMsg

    _ ->
      errorMsg
        |> Err


{-| Returns a `True` result if the argument is a `Num` or `Unit` containing a
non-zero value or a `Sides` or `Multiple` value containing at least one
non-zero value. Returns an error message if the argument contains one or more
non-numeric values.
-}
isNonZero : CssValue -> Result String Bool
isNonZero value =
  case value of
    Num number ->
      number
        |> (/=) 0
        |> Ok

    Unit number unitType ->
      number
        |> (/=) 0
        |> Ok

    Sides values ->
      values
       .|> isNonZero
        |> Helpers.resultList errorMsg
       !|> List.all identity

    Multiple separator values ->
      values
       .|> isNonZero
        |> Helpers.resultList errorMsg
       !|> List.all identity

    _ ->
      errorMsg
        |> Err


-- INTERNAL

errorMsg =
  "Operation failed because of one or more incompatible values. "
    |++ "Check to see if any of your values are non-numeric or if you are "
    |++ "trying to do arithmetic with a mix of a `Num` and a `Unit` values."