content: {-
  pandoc-crossref is a pandoc filter for numbering figures,
  equations, tables and cross-references to them.
  Copyright 2009–2014 Roel van Dijk
  Copyright 2022 Nikolay Yakimov <root@livid.pp.ru>
  
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.
  
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  
  You should have received a copy of the GNU General Public License along
  with this program; if not, write to the Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  
  This file contains code covered by the license found in licenses/LICENSE.roman-numerals
  -}
  
  {-| Parsing and pretty printing of Roman numerals.
  
  This module provides functions for parsing and pretty printing Roman
  numerals. Because the notation of Roman numerals has varied through
  the centuries this package allows for some customisation using a
  configuration that is passed to the conversion functions.
  
  Example:
  
  >>> toRoman 1729 :: String
  "MDCCXXIX"
  
  >>> fromRoman "MDCCXXIX" :: Maybe Integer
  Just 1729
  
  >>> fromRoman "Bla" :: Maybe Integer
  Nothing
  -}
  module Text.Numeral.Roman
    ( -- * Types
      NumeralConfig
    , mkNumConfig
  
      -- * Pretty printing
    , convertTo
  
      -- * Parsing
    , convertFrom
  
      -- * Default Configurations
    , modernRoman
  
      -- * Utility
    , toRoman
    , fromRoman
    ) where
  
  import Control.Monad (mzero)
  import Data.Function (on)
  import Data.List (sortBy)
  import qualified Data.Text as T (Text, null, stripPrefix)
  import Data.Text.Internal.Builder
  import qualified Data.Text.Lazy as TL (toStrict)
  
  -- |A configuration with which the 'convertTo' and 'convertFrom' functions can
  -- be parameterized.
  data NumeralConfig n = NC
    { -- |Symbol to represent the value 0.
      ncZero  :: T.Text
      -- |A table of symbols and their numerical values. The table must be
      -- ordered in descending order of the value of the symbols. If any symbol
      -- is the empty string then 'convertFrom' will be &#x22a5;.
    , ncTable :: [(T.Text, n)]
    }
  
  -- |Smart constructor for a 'NumeralConfig'.
  mkNumConfig :: (Ord n, Num n)
              => T.Text -- ^Symbol for zero
              -> T.Text -- ^Symbol for one
              -> [(T.Text, n)] -- ^ Symbol-value table.
              -> NumeralConfig n
  mkNumConfig z o tab =
    NC { ncZero = z, ncTable = sortBy (flip compare `on` snd) ((o, 1) : tab) }
  
  -- |Converts a number to a Roman numeral according to the given configuration.
  convertTo :: (Ord n, Num n) => NumeralConfig n -> n -> T.Text
  convertTo nc n | n == 0    = ncZero nc
                 | otherwise = TL.toStrict $ toLazyText $ go n $ ncTable nc
    where
      go _ [] = error "Roman.convertTo: out of symbols (BUG)"
      go i tab@(~(sym, val) : ts) | i <= 0    = mempty
                                  | i >= val  = fromText sym <> go (i - val) tab
                                  | otherwise = go i ts
  
  
  -- |Parses a string as a Roman numeral according to the given
  -- configuration. Result is 'Nothing' if the input is not a valid numeral.
  convertFrom :: (Ord n, Num n) => NumeralConfig n -> T.Text -> Maybe n
  convertFrom nc s
    | ncZero nc == s = return 0
    | otherwise = do
      n <- go 0 (ncTable nc) s
      if s == convertTo nc n then return n else mzero
    where
      go n _ x | T.null x = return n
      go _ [] _           = mzero
      go n tab@((sym, val) : ts) x =
        maybe (go n ts x) (go (n + val) tab) $ T.stripPrefix sym x
  
  -- |Configuration for Roman numerals as they are commonly used today. The value
  -- 0 is represented by the empty string. It can be interpreted as not writing
  -- down a number. This configuration is practically limited to the range
  -- [1..3999]. Smaller numbers will result in an empty string. Larger numbers
  -- will result in repeated use of the \'M\' symbol.
  modernRoman :: (Ord n, Num n) => NumeralConfig n
  modernRoman = mkNumConfig
    ""
    "I"
    [ ("IV", 4)
    , ("V" , 5)
    , ("IX", 9)
    , ("X" , 10)
    , ("XL", 40)
    , ("L" , 50)
    , ("XC", 90)
    , ("C" , 100)
    , ("CD", 400)
    , ("D" , 500)
    , ("CM", 900)
    , ("M" , 1000)
    ]
  
  -- |Converts a number to a modern Roman numeral.
  toRoman :: (Ord n, Num n) => n -> T.Text
  toRoman = convertTo modernRoman
  
  -- |Parses a string as a modern Roman numeral.
  fromRoman :: (Ord n, Num n) => T.Text -> Maybe n
  fromRoman = convertFrom modernRoman
  