packages feed

ordinal-0.6.0.0: src/Text/Numerals/Algorithm.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}

-- |
-- Module      : Text.Numerals.Algorithm
-- Description : A module that contains functions to construct algorithmic conversions from numbers to words.
-- Maintainer  : hapytexeu+gh@gmail.com
-- Stability   : experimental
-- Portability : POSIX
--
-- A module that contains data types and functions to automatically convert a number to words. It has tooling for a 'NumeralsAlgorithm'
-- as well as a 'HighNumberAlgorithm' that is used to generate a 'ShortScale' or 'LongScale'.
module Text.Numerals.Algorithm
  ( -- * Data types for number algorithms
    NumeralsAlgorithm,
    numeralsAlgorithm,

    -- * Large number algorithms
    HighNumberAlgorithm (ShortScale, LongScale),
    shortScale,
    longScale,
    shortScaleTitle,
    longScaleTitle,
    valueSplit',

    -- * Conversion to a 'NumberSegment'
    toSegments,
    toSegmentLow,
    toSegmentMid,
    toSegmentHigh,

    -- * Segment compression
    compressSegments,
  )
where

import Control.DeepSeq (NFData)
import Data.Data (Data)
import Data.Default.Class (Default (def))
import Data.Foldable (toList)
import Data.List (sortOn)
#if __GLASGOW_HASKELL__ < 803
import Data.Semigroup((<>))
#endif
import Data.Text (Text, cons, toTitle)
import Data.Vector (Vector, fromList, (!), (!?))
import qualified Data.Vector as V
import GHC.Generics (Generic)
import Test.QuickCheck (oneof)
import Test.QuickCheck.Arbitrary (Arbitrary (arbitrary, shrink))
import Text.Numerals.Class
  ( ClockText,
    FreeMergerFunction,
    FreeNumberToWords,
    FreeValueSplitter,
    MNumberSegment,
    MergerFunction,
    NumToWord (toCardinal, toOrdinal, toShortOrdinal, toTimeText'),
    NumberSegment (NumberSegment),
    NumberSegmenting,
    ValueSplit (valueSplit),
    ValueSplitter,
    toClockSegment,
    toDaySegment,
  )
import Text.Numerals.Internal (_genText, _iLogFloor, _shrinkText, _thousand)
import Text.Numerals.Prefix (latinPrefixes)

-- | A data type for algorithmic number to word conversions. Most western
-- languages /likely/ can work with this data type.
data NumeralsAlgorithm = NumeralsAlgorithm
  { -- | The word used as prefix to denote negative numbers.
    minusWord :: Text,
    -- | The word used to denote /one/ in the language.
    oneWord :: Text,
    -- | A 'Vector' of small numbers, the first item is the word for /two/ and each successor is the word for the next number.
    lowWords :: Vector Text,
    -- | A list of 2-tuples where the first item contains the value, and the second the corresponding word, the values are ordered in descending value order.
    midWords :: [(Integer, Text)],
    -- | A function that is used to generate words for large values (greater than or equal to one /million/), often constructed with the /short scale/ or /long scale/.
    highWords :: FreeValueSplitter,
    -- | A function that specifies how to merge words based on the grammar of that specific language.
    mergeFunction :: FreeMergerFunction,
    -- | A function to conver the /cardinal/ form of a number in an /ordinal/ one.
    ordinize :: Text -> Text,
    -- | A function that converts a number to its /short ordinal/ form.
    shortOrdinal :: FreeNumberToWords,
    -- | A function that converts the clock segment and day segment to a /Text/ that describes the time of the day in words.
    clockText :: ClockText
  }

instance NumToWord NumeralsAlgorithm where
  toCardinal NumeralsAlgorithm {minusWord = _minusWord, oneWord = _oneWord, lowWords = _lowWords, midWords = _midWords, highWords = _highWords, mergeFunction = _mergeFunction} = cardinal
    where
      cardinal i
        | i < 0 = _minusWord <> cons ' ' (go (-j))
        | otherwise = go j
        where
          go = compressSegments _oneWord _mergeFunction . toSegments _lowWords _midWords _highWords
          j = fromIntegral i :: Integer

  toOrdinal na@NumeralsAlgorithm {ordinize = _ordinize} = _ordinize . toCardinal na
  toShortOrdinal = shortOrdinal
  toTimeText' alg h m = clockText alg (toClockSegment m) (toDaySegment h) h m

_toNumberScale :: (Integral i, Integral j) => i -> (j, i)
_toNumberScale i = (l, k)
  where
    ~(_, l, k) = _iLogFloor _thousand i

-- | A data type used for to map larger numbers to words. This data type
-- supports the /short scale/ and /long scale/ with /Latin/ prefixes, and
-- custom suffixes. The 'Default' value is the /short scale/ with /illion/
-- as suffix. This is used in /English/ for large numbers.
data HighNumberAlgorithm
  = ShortScale Text
  | LongScale Text Text
  deriving (Data, Eq, Generic, Ord, Read, Show)

instance NFData HighNumberAlgorithm

instance Arbitrary HighNumberAlgorithm where
  arbitrary = oneof [ShortScale <$> _genText, LongScale <$> _genText <*> _genText]
  shrink (ShortScale t) = ShortScale <$> _shrinkText t
  shrink (LongScale ta tb) = ((`LongScale` tb) <$> _shrinkText ta) <> (LongScale ta <$> _shrinkText tb)

instance Default HighNumberAlgorithm where
  def = ShortScale "illion"

-- | Construct a 'FreeValueSplitter' function for the given suffix for a /short scale/.
shortScale :: Text -> FreeValueSplitter
shortScale = valueSplit . ShortScale

-- | Construct a 'FreeValueSplitter' function for the given suffixes for a /long scale/.
longScale :: Text -> Text -> FreeValueSplitter
longScale suf1 = valueSplit . LongScale suf1

-- | Construct a 'FreeValueSplitter' function for the given suffix for a /short scale/, the names are written in /title case/.
shortScaleTitle :: Text -> FreeValueSplitter
shortScaleTitle = valueSplit' toTitle . ShortScale

-- | Construct a 'FreeValueSplitter' function for the given suffixes for a /long scale/, the names are written in /title case/.
longScaleTitle :: Text -> Text -> FreeValueSplitter
longScaleTitle suf1 = valueSplit' toTitle . LongScale suf1

_highWithSuffix :: Text -> Int -> Maybe Text
_highWithSuffix suf = fmap (<> suf) . (latinPrefixes !?)

_highToText :: HighNumberAlgorithm -> Int -> Maybe Text
_highToText (ShortScale suf) j = _highWithSuffix suf j
_highToText (LongScale suf1 suf2) j
  | even j = _highWithSuffix suf1 k
  | otherwise = _highWithSuffix suf2 k
  where
    k = div j 2

-- | Generate a /value splitter/ for a 'HighNumberAlgorithm' but where the result
-- is post-processed by a function.
valueSplit' ::
  -- | The post-processing function.
  (Text -> Text) ->
  -- | The 'HighNumberAlgorithm' that is used.
  HighNumberAlgorithm ->
  -- | The 'FreeValueSplitter' result.
  FreeValueSplitter
valueSplit' f vs i = (m,) . f <$> _highToText vs (j - 2)
  where
    ~(j, m) = _toNumberScale i

instance ValueSplit HighNumberAlgorithm where
  valueSplit = valueSplit' id

-- | A /smart constructor/ for the 'NumeralsAlgorithm' type. This constructor
-- allows one to use an arbitrary 'Foldable' type for the low words and mid
-- words. It will also order the midwords accordingly.
numeralsAlgorithm :: (Foldable f, Foldable g) => Text -> Text -> Text -> f Text -> g (Integer, Text) -> FreeValueSplitter -> FreeMergerFunction -> (Text -> Text) -> FreeNumberToWords -> ClockText -> NumeralsAlgorithm
numeralsAlgorithm minus zero one _lowWords _midWords = NumeralsAlgorithm minus one (fromList (zero : one : toList _lowWords)) (sortOn (negate . fst) (toList _midWords))

_maybeSegment :: Integral i => (i -> NumberSegment i) -> i -> MNumberSegment i
_maybeSegment f = go
  where
    go 0 = Nothing
    go i = Just (f i)

-- | Convert the given number to a 'NumberSegment' with the given 'Vector' of
-- low numbers. Mid words and large numbers are not taken into account. This
-- is often the next step after the 'toSegmentMid'.
toSegmentLow ::
  Integral i =>
  -- | A 'Vector' of low words.
  Vector Text ->
  -- | The function that maps the number to the 'NumberSegment'.
  NumberSegmenting i
toSegmentLow vs = go
  where
    go i
      | i >= nvs = NumberSegment (Just (go dv)) nvs lv (tl md)
      | otherwise = NumberSegment Nothing i (vs ! fromIntegral i) Nothing
      where
        (dv, md) = divMod i nvs
    lv = V.last vs
    nvs = fromIntegral (V.length vs) - 1
    tl = _maybeSegment go

_splitRecurse :: Integral i => (i -> NumberSegment i) -> (i -> NumberSegment i) -> i -> Text -> i -> NumberSegment i
_splitRecurse f g im v j = NumberSegment hd im v (_maybeSegment g md)
  where
    hd
      | dv == 1 = Nothing
      | otherwise = Just (f dv)
    ~(dv, md) = divMod j im

-- | Convert the given number to a 'NumberSegment' with the given 'Vector' of
-- low numbers, and the /sorted/ list of mid numbers. Large numbers are not
-- taken into account. This is often the next step after the 'toSegmentHigh'.
toSegmentMid ::
  Integral i =>
  -- | A 'Vector' of low words.
  Vector Text ->
  -- | The list of name and the names of these numbers in /descending/ order for the mid words.
  [(Integer, Text)] ->
  -- | The function that maps the number to the 'NumberSegment'.
  NumberSegmenting i
toSegmentMid lows = go
  where
    go [] n = toSegmentLow lows n
    go ma@((m, v) : ms) n
      | im > n = goms n
      | otherwise = _splitRecurse (go ma) goms im v n
      where
        im = fromIntegral m
        goms = go ms

-- | Convert the given number to a 'NumberSegment' with the given 'Vector' of
-- low numbers, the /sorted/ list of mid numbers, and a 'FreeValueSplitter' for
-- large numbers.
toSegmentHigh ::
  Integral i =>
  -- | A 'Vector' of low words.
  Vector Text ->
  -- | The list of name and the names of these numbers in /descending/ order for the mid words.
  [(Integer, Text)] ->
  -- | The 'ValueSplitter' used for large numbers, likely a splitter from a /short scale/ or /long scale/.
  ValueSplitter i ->
  -- | The function that maps the number to the 'NumberSegment'.
  NumberSegmenting i
toSegmentHigh lows mids highs = go
  where
    go v
      | Just (i, t) <- highs v = _splitRecurse go go i t v
      | otherwise = toSegmentMid lows mids v

-- | Convert the given number to a 'NumberSegment' with the given 'Vector' of
-- low numbers, the /sorted/ list of mid numbers, and a 'FreeValueSplitter' for
-- large numbers.
toSegments ::
  Integral i =>
  -- | A 'Vector' of low words.
  Vector Text ->
  -- | The list of name and the names of these numbers in /descending/ order for the mid words.
  [(Integer, Text)] ->
  -- | The 'ValueSplitter' used for large numbers, likely a splitter from a /short scale/ or /long scale/.
  ValueSplitter i ->
  -- | The function that maps the number to the 'NumberSegment'.
  NumberSegmenting i
toSegments = toSegmentHigh

-- | Use the given 'MergerFunction' to compress the 'NumberSegment' to a single
-- 'Text' object that represents the given number.
compressSegments ::
  Integral i =>
  -- | The value used for /one/ in the specific language.
  Text ->
  -- | The 'MergerFunction' for the specific language that implements the grammar rules how to merge values.
  MergerFunction i ->
  -- | The given 'NumberSegment' value to turn into a 'Text' object.
  NumberSegment i ->
  -- | The 'Text' object that contains the name of the number stored in the 'NumberSegment'.
  Text
compressSegments one' merger = snd . go
  where
    go (NumberSegment dv' i t md') = _mergeTail md' (dvi * i, merger dvi i dv t)
      where
        (dvi, dv) = _unwrap dv'
    _unwrap = maybe (1, one') go
    _mergeTail Nothing r = r
    _mergeTail (Just md') (vi, v) = (vi + mdi, merger vi mdi v md)
      where
        (mdi, md) = go md'