{-# LANGUAGE Safe #-} {-| Module : Data.Char.Roman Description : A module to print Roman numerals both in uppercase and lowercase. Maintainer : hapytexeu+gh@gmail.com Stability : experimental Portability : POSIX This module aims to convert Roman numerals to a String of unicode characters that represent Roman numerals. One can convert numbers to Roman numerals in uppercase and lowercase, and in 'Additive' and 'Subtractive' style. -} module Data.Char.Roman ( -- * Data types to represent Roman numerals RomanLiteral(I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, L, C, D, M) , RomanStyle(Additive, Subtractive) -- * Convert a number to Roman literals , toLiterals , romanLiteral, romanLiteral' -- * Convert a number to text , romanNumeral, romanNumeral' , romanNumber, romanNumber' ) where import Data.Bits((.|.)) import Data.Char(chr) import Data.Char.Core(Ligate, ligateF) import Data.Default(Default(def)) import Data.Text(Text, cons, empty) import Test.QuickCheck.Arbitrary(Arbitrary(arbitrary), arbitraryBoundedEnum) -- | The style to convert a number to a Roman numeral. data RomanStyle = Additive -- ^ The additive style converts four to ⅠⅠⅠⅠ. | Subtractive -- ^ The subtractive style converts four to ⅠⅤ. deriving (Bounded, Enum, Eq, Show, Read) instance Default RomanStyle where def = Subtractive instance Arbitrary RomanStyle where arbitrary = arbitraryBoundedEnum -- | Roman numerals for which a unicode character exists. data RomanLiteral = I -- ^ The unicode character for the Roman numeral /one/: Ⅰ. | II -- ^ The unicode character for the Roman numeral /two/: Ⅱ. | III -- ^ The unicode character for the Roman numeral /three/: Ⅲ. | IV -- ^ The unicode character for the Roman numeral /four/: Ⅳ. | V -- ^ The unicode character for the Roman numeral /five/: Ⅴ. | VI -- ^ The unicode character for the Roman numeral /six/: Ⅵ. | VII -- ^ The unicode character for the Roman numeral /seven/: Ⅶ. | VIII -- ^ The unicode character for the Roman numeral /eight/: Ⅷ. | IX -- ^ The unicode character for the Roman numeral /nine/: Ⅸ. | X -- ^ The unicode character for the Roman numeral /ten/: Ⅹ. | XI -- ^ The unicode character for the Roman numeral /eleven/: Ⅺ. | XII -- ^ The unicode character for the Roman numeral /twelve/: Ⅻ. | L -- ^ The unicode character for the Roman numeral /fifty/: Ⅼ. | C -- ^ The unicode character for the Roman numeral /hundred/: Ⅽ. | D -- ^ The unicode character for the Roman numeral /five hundred/: Ⅾ. | M -- ^ The unicode character for the Roman numeral /thousand/: Ⅿ. deriving (Bounded, Enum, Eq, Show, Read) _literals :: Integral i => RomanStyle -> [(i, [RomanLiteral] -> [RomanLiteral])] _literals Additive = [ (1000, (M:)) , (500, (D:)) , (100, (C:)) , (50, (L:)) , (10, (X:)) , (5, (V:)) , (1, (I:)) ] _literals Subtractive = [ (1000, (M:)) , (900, ([C,M]++)) , (500, (D:)) , (400, ([C,D]++)) , (100, (C:)) , (90, ([X,C]++)) , (50, (L:)) , (40, ([X,L]++)) , (10, (X:)) , (9, ([I,X]++)) , (5, (V:)) , (4, ([I,V]++)) , (1, (I:)) ] _ligate :: [RomanLiteral] -> [RomanLiteral] _ligate [] = [] _ligate (r:rs) = go r rs where go x [] = [x] go x (y:ys) = f x y ys f I I = go II f II I = skip III f I V = skip IV f V I = go VI f VI I = go VII f VII I = skip VIII f X I = go XI f I X = skip IX f XI I = go XII f x y = (x :) . go y skip = (. _ligate) . (:) -- | Convert the given number with the given 'RomanStyle' and 'Ligate' style -- to a sequence of 'RomanLiteral's, given the number can be represented -- with Roman numerals (is strictly larger than zero). toLiterals :: Integral i => RomanStyle -- ^ Specifies if the Numeral is 'Additive' or 'Subtractive' style. -> Ligate -- ^ Specifies if characters like @ⅠⅤ@ are joined to @Ⅳ@. -> i -- ^ The given number to convert. -> Maybe [RomanLiteral] -- ^ A list of 'RomanLiteral's if the given number can be specified -- with Roman numerals, 'Nothing' otherwise. toLiterals s c k | k > 0 = ligateF _ligate c (go k (_literals s)) | otherwise = Nothing where go 0 _ = Just [] go _ [] = Nothing go n va@((m, l):vs) | n >= m = l <$> go (n-m) va | otherwise = go n vs _romanLiteral :: Int -> RomanLiteral -> Char _romanLiteral = (chr .) . (. fromEnum) . (.|.) -- | Convert the given 'RomanLiteral' object to a unicode character in -- /uppercase/. romanLiteral :: RomanLiteral -- ^ The given 'RomanLiteral' to convert. -> Char -- ^ A unicode character that represents the given 'RomanLiteral'. romanLiteral = _romanLiteral 0x2160 -- | Convert the given 'RomanLiteral' object to a unicode character in -- /lowercase/. romanLiteral' :: RomanLiteral -- ^ The given 'RomanLiteral' to convert. -> Char -- ^ A unicode character that represents the given 'RomanLiteral'. romanLiteral' = _romanLiteral 0x2170 _romanNumeral :: (RomanLiteral -> Char) -> [RomanLiteral] -> Text _romanNumeral = (`foldr` empty) . (cons .) -- | Convert a sequence of 'RomanLiteral' objects to a 'Text' object that -- contains a sequence of corresponding Unicode characters which are Roman -- numberals in /uppercase/. romanNumeral :: [RomanLiteral] -- ^ The given list of 'RomanLiteral' objects to convert to a Unicode equivalent. -> Text -- ^ A 'Text' object that contains a sequence of unicode characters that represents the 'RomanLiteral's. romanNumeral = _romanNumeral romanLiteral -- | Convert a sequence of 'RomanLiteral' objects to a 'Text' object that -- contains a sequence of corresponding Unicode characters which are Roman -- numberals in /lowercase/. romanNumeral' :: [RomanLiteral] -- ^ The given list of 'RomanLiteral' objects to convert to a Unicode equivalent. -> Text -- ^ A 'Text' object that contains a sequence of unicode characters that represents the 'RomanLiteral's. romanNumeral' = _romanNumeral romanLiteral' _romanNumber :: Integral i => ([RomanLiteral] -> a) -> RomanStyle -> Ligate -> i -> Maybe a _romanNumber f r c = fmap f . toLiterals r c -- | Convert a given number to a 'Text' wrapped in a 'Just' data constructor, -- given the number can be converted to a Roman numeral, given it can be -- represented. 'Nothing' in case it can not be represented. The number is -- written in Roman numerals in /uppercase/. romanNumber :: Integral i => RomanStyle -- ^ Specifies if the Numeral is 'Additive' or 'Subtractive' style. -> Ligate -- ^ Specifies if characters like @ⅠⅤ@ are joined to @Ⅳ@. -> i -- ^ The given number to convert. -> Maybe Text -- ^ A 'Text' if the given number can be specified with Roman -- numerals wrapped in a 'Just', 'Nothing' otherwise. romanNumber = _romanNumber romanNumeral -- | Convert a given number to a 'Text' wrapped in a 'Just' data constructor, -- given the number can be converted to a Roman numeral, given it can be -- represented. 'Nothing' in case it can not be represented. The number is -- written in Roman numerals in /lowercase/. romanNumber' :: Integral i => RomanStyle -- ^ Specifies if the Numeral is 'Additive' or 'Subtractive' style. -> Ligate -- ^ Specifies if characters like @ⅠⅤ@ are joined to @Ⅳ@. -> i -- ^ The given number to convert. -> Maybe Text -- ^ A 'Text' if the given number can be specified with Roman -- numerals wrapped in a 'Just', 'Nothing' otherwise. romanNumber' = _romanNumber romanNumeral'