packages feed

fluent-1.1.0: src/Language/Fluent/Function.hs

module Language.Fluent.Function where

import Data.Either.Extra (maybeToEither)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.List qualified as List
import Data.List.NonEmpty (NonEmpty (..))
import Data.Monoid (Endo (..))
import Data.Text (Text)
import Data.Text qualified as Text
import Language.Fluent.AST (Identifier (..))
import Language.Fluent.Number (NumberOptions (..), Style (..))
import Language.Fluent.Pattern (Pattern (..), PatternElement (..))
import Language.Fluent.Pattern qualified as Pattern
import Language.Fluent.Time (Digits, TimeOptions (..))
import Language.Fluent.Value (Value, asText)
import Language.Fluent.Value qualified as Value
import Language.Fluent.Width (Width)
import Text.Read (readMaybe)
import Prelude

builtins
    :: HashMap
        Identifier
        ( [Pattern]
          -> HashMap Identifier Value
          -> Either String Pattern
        )
builtins = HashMap.fromList [(Identifier "NUMBER", number), (Identifier "DATETIME", datetime)]

-- | @NUMBER($ratio, minimumFractionDigits: 2)@
number :: [Pattern] -> HashMap Identifier Value -> Either String Pattern
number positional args = do
    given <- maybeToEither "NUMBER: expected one positional argument" $ lone positional
    case given of
        Value.Number value opts -> do
            change <- changes numberOption args
            style <- numberStyle opts.style
            pure . Pattern.fromValue . Value.Number value $ (appEndo change opts){style}
        Value.Invalid reason -> Left reason
        _ -> Left "NUMBER: the positional argument is not a number"
  where
    option name = asText <$> HashMap.lookup (Identifier name) args

    numberStyle :: Style -> Either String Style
    numberStyle current = case (option "style", option "currency", option "unit") of
        (Just "decimal", _, _) -> Right Decimal
        (Just "percent", _, _) -> Right Percent
        (Just "currency", currency, _) ->
            maybe (Left "NUMBER: the currency style needs a currency") (Right . Currency) currency
        (Just "unit", _, unit) ->
            maybe (Left "NUMBER: the unit style needs a unit") (Right . Unit) unit
        (Just it, _, _) -> Left $ "NUMBER: no such style: " <> Text.unpack it
        (Nothing, Just currency, _) -> Right $ Currency currency
        (Nothing, Nothing, Just unit) -> Right $ Unit unit
        (Nothing, Nothing, Nothing) -> Right current

    numberOption :: Identifier -> Value -> Either String (Endo NumberOptions)
    numberOption (Identifier name) (asText -> text) = case name of
        "style" -> Right mempty
        "currency" -> Right mempty
        "unit" -> Right mempty
        "type" -> set (\it options -> options{form = it}) parsed
        "currencyDisplay" -> set (\it options -> options{currencyDisplay = it}) parsed
        "unitDisplay" -> set (\it options -> options{unitDisplay = it}) parsed
        "useGrouping" -> set (\it options -> options{useGrouping = it}) $ flag text
        "minimumIntegerDigits" ->
            set (\it options -> options{minimumIntegerDigits = it}) $ count text
        "minimumFractionDigits" ->
            set (\it options -> options{minimumFractionDigits = it}) $ count text
        "maximumFractionDigits" ->
            set (\it options -> options{maximumFractionDigits = Just it}) $ count text
        "minimumSignificantDigits" ->
            set (\it options -> options{minimumSignificantDigits = Just it}) $ count text
        "maximumSignificantDigits" ->
            set (\it options -> options{maximumSignificantDigits = Just it}) $ count text
        it -> Left $ "NUMBER: no such option: " <> Text.unpack it
      where
        parsed :: (Bounded a, Enum a, Read a, Show a) => Either String a
        parsed = named "NUMBER" name text

    count :: Text -> Either String Int
    count (Text.unpack -> it) = maybe (Left $ "not a count: " <> it) Right . readMaybe $ it

-- | @DATETIME($date, month: "long")@
datetime :: [Pattern] -> HashMap Identifier Value -> Either String Pattern
datetime positional args = do
    given <- maybeToEither "DATETIME: expected one positional argument" $ lone positional
    case given of
        Value.Time value opts -> do
            change <- changes timeOption args
            pure . Pattern.fromValue . Value.Time value $ appEndo change opts
        Value.Invalid reason -> Left reason
        _ -> Left "DATETIME: the positional argument is not a time"
  where
    timeOption :: Identifier -> Value -> Either String (Endo TimeOptions)
    timeOption (Identifier name) (asText -> text) = case name of
        "timeZone" -> set (\it options -> options{timeZone = it}) $ zone text
        "hour12" -> set (\it options -> options{hour12 = Just it}) $ flag text
        "weekday" -> set (\it options -> options{weekday = Just it}) parsed
        "era" -> set (\it options -> options{era = Just it}) parsed
        "year" -> set (\it options -> options{year = Just it}) parsed
        "month" -> set (\it options -> options{month = Just it}) monthField
        "day" -> set (\it options -> options{day = Just it}) parsed
        "hour" -> set (\it options -> options{hour = Just it}) parsed
        "minute" -> set (\it options -> options{minute = Just it}) parsed
        "second" -> set (\it options -> options{second = Just it}) parsed
        "timeZoneName" -> set (\it options -> options{timeZoneName = Just it}) parsed
        it -> Left $ "DATETIME: no such option: " <> Text.unpack it
      where
        parsed :: (Bounded a, Enum a, Read a, Show a) => Either String a
        parsed = named "DATETIME" name text
        monthField
            | Right digits <- parsed @Digits = Right $ Left digits
            | Right width <- parsed @Width = Right $ Right width
            | otherwise = Left $ "DATETIME: no such month: " <> Text.unpack text

    zone :: Text -> Either String Text
    zone "" = Left "DATETIME: the timeZone names no zone"
    zone it = Right it

changes
    :: (Identifier -> Value -> Either String (Endo options))
    -> HashMap Identifier Value
    -> Either String (Endo options)
changes option = fmap mconcat . traverse (uncurry option) . HashMap.toList

named :: forall a. (Bounded a, Enum a, Read a, Show a) => String -> Text -> Text -> Either String a
named fn (Text.unpack -> what) (Text.unpack -> text) = maybeToEither err $ readMaybe text
  where
    err =
        mconcat
            [ fn
            , ": expected one of "
            , List.intercalate ", " (show <$> [minBound @a .. maxBound])
            , " for "
            , what
            , ", but got "
            , text
            ]

set :: (a -> options -> options) -> Either String a -> Either String (Endo options)
set change = fmap $ Endo . change

lone :: [Pattern] -> Maybe Value
lone [Pattern (Value it :| [])] = Just it
lone _ = Nothing

flag :: Text -> Either String Bool
flag (Text.unpack -> it)
    | it `elem` ["true", "1"] = Right True
    | it `elem` ["false", "0"] = Right False
    | otherwise = Left $ "not a flag: " <> it