packages feed

fluent-1.1.0: test/BuiltinSpec.hs

module BuiltinSpec (spec) where

import Data.Foldable (for_)
import Data.Functor (void)
import Data.HashMap.Strict qualified as HashMap
import Data.List.Extra (enumerate)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text qualified as Text
import Data.Time (UTCTime (..), fromGregorian, secondsToDiffTime)
import Language.Fluent.Function qualified as Function
import Language.Fluent.Pattern qualified as Pattern
import Language.Fluent.Value qualified as Value
import Prelude

spec :: Spec
spec = do
    describe "NUMBER" do
        it "formats a number by the given options" $
            withNumber [fluent|foo = { NUMBER($arg, minimumFractionDigits: 2) }|]
                `shouldBe` Right "3.00"
        it "reads the minimum integer digits" $
            numberOptionsWith [("minimumIntegerDigits", "3")]
                `shouldBe` Right numberOptions{minimumIntegerDigits = 3}
        for_
            [ "minimumIntegerDigits"
            , "minimumFractionDigits"
            , "maximumFractionDigits"
            , "minimumSignificantDigits"
            ]
            \option ->
                it (Text.unpack $ "formats a number with " <> option <> " too large to honour") $
                    void (withNumber $ hugeOption option) `shouldBe` Right ()
        it "reads the percent style" $
            numberOptionsWith [("style", "percent")] `shouldBe` Right numberOptions{style = Percent}
        it "reads the currency style and how to display the currency" $
            numberOptionsWith
                [("style", "currency"), ("currency", "EUR"), ("currencyDisplay", "code")]
                `shouldBe` Right numberOptions{style = Currency "EUR", currencyDisplay = Code}
        it "reads the unit style" $
            numberOptionsWith [("style", "unit"), ("unit", "metre")]
                `shouldBe` Right numberOptions{style = Unit "metre"}
        it "ignores a unit where the style is currency" $
            withNumber
                [fluent|
                  foo = { NUMBER($arg, style: "currency", currency: "EUR", unit: "kilometre") }
                |]
                `shouldBe` withNumber
                    [fluent|foo = { NUMBER($arg, style: "currency", currency: "EUR") }|]
        it "rejects the currency style without a currency" $
            withNumber [fluent|foo = { NUMBER($arg, style: "currency") }|]
                `shouldBe` Left "NUMBER: the currency style needs a currency"
        it "rejects the unit style without a unit" $
            withNumber [fluent|foo = { NUMBER($arg, style: "unit") }|]
                `shouldBe` Left "NUMBER: the unit style needs a unit"
        it "selects by the ordinal category of the number when asked to" $
            withNumber
                [fluent|
                  foo = { NUMBER($arg, type: "ordinal") ->
                      [few] few
                     *[other] other
                    }
                |]
                `shouldBe` Right "few"
        it "selects by the plural category of the number" $
            withNumber
                [fluent|
                  foo = { NUMBER($arg, minimumFractionDigits: 1) ->
                      [one] one
                     *[other] other
                    }
                |]
                `shouldBe` Right "other"

    describe "DATETIME" do
        it "reads the fields it is given" $ do
            timeOptionsWith [("year", "numeric")] `shouldBe` Right timeOptions{year = Just Numeric}
            timeOptionsWith [("month", "long")] `shouldBe` Right timeOptions{month = Just $ Right Long}
            timeOptionsWith [("weekday", "long")] `shouldBe` Right timeOptions{weekday = Just Long}
        it "reads whether to use a 12-hour clock" $ do
            timeOptionsWith [("hour12", "true")] `shouldBe` Right timeOptions{hour12 = Just True}
            timeOptionsWith [("hour12", "false")] `shouldBe` Right timeOptions{hour12 = Just False}
        it "rejects a value it does not know" $
            withTime (monthOf "banana") `shouldBe` Left "DATETIME: no such month: banana"
        for_ (enumerate @Width) \width ->
            it (Text.unpack $ "reads the month width " <> Text.show width) $
                timeOptionsWith [("month", toValue $ show width)]
                    `shouldBe` Right timeOptions{month = Just $ Right width}
        for_ (enumerate @Digits) \digits ->
            it (Text.unpack $ "reads the month digits " <> Text.show digits) $
                timeOptionsWith [("month", toValue $ show digits)]
                    `shouldBe` Right timeOptions{month = Just $ Left digits}

hugeOption :: Text -> Resource
hugeOption option =
    either error id . parseResource $
        "foo = { NUMBER($arg, " <> option <> ": 1000000000) }"

monthOf :: Text -> Resource
monthOf month =
    either error id . parseResource $
        "foo = { DATETIME($arg, month: \"" <> month <> "\", year: \"numeric\") }"

withNumber :: Resource -> Either String Text
withNumber = translate "foo" ("arg", 3 :: Int)

withTime :: Resource -> Either String Text
withTime = translate "foo" ("arg", afternoon)

numberOptionsWith :: [(Identifier, Value)] -> Either String NumberOptions
numberOptionsWith named = do
    result <- Function.number [Pattern.fromValue (3 :: Int)] $ HashMap.fromList named
    case result of
        Pattern (Value (Value.Number _ options) :| []) -> Right options
        _ -> Left "not a number"

timeOptionsWith :: [(Identifier, Value)] -> Either String TimeOptions
timeOptionsWith named = do
    result <- Function.datetime [Pattern.fromValue afternoon] $ HashMap.fromList named
    case result of
        Pattern (Value (Value.Time _ options) :| []) -> Right options
        _ -> Left "not a time"

afternoon :: UTCTime
afternoon = UTCTime (fromGregorian 2026 8 10) . secondsToDiffTime $ 12 * 3600 + 34 * 60 + 56