ordinal-0.5.0.0: src/Text/Numerals/Class.hs
{-# LANGUAGE CPP, DeriveDataTypeable, DeriveFunctor, DeriveFoldable, DeriveGeneric, RankNTypes, Safe #-}
{-|
Module : Text.Numerals.Class
Description : A module that contains the typeclasses on which the rest of the module works.
Maintainer : hapytexeu+gh@gmail.com
Stability : experimental
Portability : POSIX
A module that defines the typeclasses that are used in the rest of the module. The 'NumToWord' class
is the typeclass that is used by all algorithmic conversion tools.
-}
module Text.Numerals.Class (
-- * Typeclasses
NumToWord(toCardinal, toOrdinal, toShortOrdinal, toWords, toTimeText, toTimeText')
, ValueSplit(valueSplit)
-- * Types of numbers
, NumberType(Cardinal, Ordinal, ShortOrdinal)
-- * Segmenting a number
, NumberSegment(NumberSegment, segmentDivision, segmentValue, segmentText, segmentRemainder)
, MNumberSegment
-- * Segments of time
, ClockSegment(OClock, Past, QuarterPast, ToHalf, Half, PastHalf, QuarterTo, To)
, DayPart(Night, Morning, Afternoon, Evening)
, DaySegment(DaySegment, dayPart, dayHour)
, toDayPart, toDaySegment, toClockSegment
, hourCorrection
-- * Convert the current time to words
, currentTimeText, currentTimeText'
-- * Utility type synonyms
, NumberToWords, FreeNumberToWords
, MergerFunction, FreeMergerFunction, ValueSplitter, FreeValueSplitter, NumberSegmenting
, ClockText
) where
import Control.DeepSeq(NFData, NFData1)
import Data.Data(Data)
import Data.Default.Class(Default(def))
#if __GLASGOW_HASKELL__ < 803
import Data.Semigroup((<>))
#endif
import Data.Text(Text)
import Data.Time.Clock(getCurrentTime, utctDayTime)
import Data.Time.LocalTime(TimeOfDay(TimeOfDay), TimeZone, timeToTimeOfDay, utcToLocalTimeOfDay)
import GHC.Generics(Generic, Generic1)
import Test.QuickCheck(choose)
import Test.QuickCheck.Arbitrary(Arbitrary(arbitrary, shrink), Arbitrary1(liftArbitrary), arbitrary1, arbitraryBoundedEnum)
import Text.Numerals.Internal(_genText, _shrinkText)
-- | A type alias for a function that maps a number to a 'Text' object.
type NumberToWords i = i -> Text
-- | A type alias for a 'NumberToWords' function, with a free 'Integral'
-- variable.
type FreeNumberToWords = forall i . Integral i => NumberToWords i
-- | A type alias of a function that is used to merge the names of two numbers according
-- to gramatical rules. The type parameter is the type of the numbers to merge.
type MergerFunction i = i -> i -> Text -> Text -> Text
-- | A type alias of a 'MergerFunction' function with a free 'Integral' variable.
type FreeMergerFunction = forall i . Integral i => MergerFunction i
-- | A type alias of a function that maps a number to a 2-tuple that contains a
-- number and the word for that number. This number is normally the largest
-- number smaller than the given number. In case no name exists for a number
-- smaller than the given one 'Nothing' is returned.
type ValueSplitter i = i -> Maybe (i, Text)
-- | A type alias of a 'ValueSplitter' function, with a free 'Integral'
-- variable.
type FreeValueSplitter = forall i . Integral i => ValueSplitter i
-- | A type alias of a function that converts a number to a 'NumberSegment' for that number.
type NumberSegmenting i = i -> NumberSegment i
-- | A data type used to convert a number into segments. Each segment has an
-- optional division and remainder part, together with a value and the name of
-- that value in a language.
data NumberSegment i = NumberSegment {
segmentDivision :: MNumberSegment i -- ^ The optional division part. 'Nothing' if the division is equal to one.
, segmentValue :: i -- ^ The value of the given segment.
, segmentText :: Text -- ^ The name of the value of the given segment, in a specific language.
, segmentRemainder :: MNumberSegment i -- ^ The optional remainder part. 'Nothing' if the remainder is equal to zero.
} deriving (Data, Eq, Foldable, Functor, Generic, Generic1, Ord, Read, Show)
instance NFData a => NFData (NumberSegment a)
instance NFData1 NumberSegment
instance Arbitrary1 NumberSegment where
liftArbitrary gen = go
where go = NumberSegment <$> liftArbitrary go <*> gen <*> _genText <*> liftArbitrary go
instance Arbitrary i => Arbitrary (NumberSegment i) where
arbitrary = arbitrary1
shrink (NumberSegment dv val txt rm) =
((\x -> NumberSegment x val txt rm) <$> shrink dv) <>
((\x -> NumberSegment dv x txt rm) <$> shrink val) <>
((\x -> NumberSegment dv val x rm) <$> _shrinkText txt) <>
(NumberSegment dv val txt <$> shrink rm)
-- | A 'Maybe' variant of the 'NumberSegment' data type. This is used since the
-- division part can be one, or the remainder part can be zero.
type MNumberSegment i = Maybe (NumberSegment i)
-- | A data type that specifies the different types of numbers. These can be
-- used to specify the "target format". The 'Default' number type is 'Cardinal'.
data NumberType
= Cardinal -- ^ /Cardinal/ numbers like one, two, three, etc.
| Ordinal -- ^ /Ordinal/ numbers like first, second, third, etc.
| ShortOrdinal -- ^ /Short ordinal/ numbers like 1st, 2nd, 3rd, etc.
deriving (Bounded, Data, Enum, Eq, Generic, Ord, Read, Show)
instance Arbitrary NumberType where
arbitrary = arbitraryBoundedEnum
instance NFData NumberType
-- | The type of a function that converts time to its description. The first
-- two parameters are used to make conversion more convenient.
type ClockText
= ClockSegment -- ^ The 'ClockSegment' that describes the state of minutes within an hour.
-> DaySegment -- ^ The 'DaySegment' that describes the state of hours within a day.
-> Int -- ^ The number of hours.
-> Int -- ^ The number of minutes.
-> Text -- ^ A 'Text' object that describes the given time.
-- | A data type that describes the state of the minutes within an hour.
data ClockSegment
= OClock -- ^ The number of minutes is zero.
| Past Int -- ^ The parameter is the number of minutes past the hour, this is between @1@ and @14@.
| QuarterPast -- ^ It is a quarter past the hour.
| ToHalf Int -- ^ The parameter is the number of minutes until half, this is between @1@ and @14@.
| Half -- ^ It is half past an hour.
| PastHalf Int -- ^ The parameter is the number of minutes past half, this is between @1@ and @14@.
| QuarterTo -- ^ It is a quarter to an hour.
| To Int -- ^ The parameter is the number of minutes to the next hour, this is between @1@ and @14@.
deriving (Data, Eq, Generic, Ord, Read, Show)
instance NFData ClockSegment
instance Arbitrary ClockSegment where
arbitrary = toClockSegment <$> choose (0, 59)
-- | A data type that describes the state of the hours within a day.
data DayPart
= Night -- ^ It is night, this means that it is between @0:00@ and @5:59@.
| Morning -- ^ It is morning, this means that it is between @6:00@ and @11:59@.
| Afternoon -- ^ It is afternoon, this means it is between @12:00@ and @17:59@.
| Evening -- ^ It is evening, this means it is between @18:00@ and @23:59@.
deriving (Bounded, Data, Enum, Eq, Generic, Ord, Read, Show)
instance Arbitrary DayPart where
arbitrary = arbitraryBoundedEnum
instance NFData DayPart
-- | A data type that describes the part of the day, and the number of hours on
-- a 12-hour clock.
data DaySegment
= DaySegment {
dayPart :: DayPart -- ^ The part of the day.
, dayHour :: Int -- ^ The number of hours, between @1@ and @12@ (both inclusive).
}
deriving (Data, Eq, Generic, Ord, Read, Show)
instance Arbitrary DaySegment where
arbitrary = toDaySegment <$> choose (0, 23)
instance NFData DaySegment
-- | Convert the given number of minutes to the corresponding 'ClockSegment'.
toClockSegment
:: Int -- ^ The number of minutes.
-> ClockSegment -- ^ The corresponding 'ClockSegment'.
toClockSegment 0 = OClock
toClockSegment 15 = QuarterPast
toClockSegment 30 = Half
toClockSegment 45 = QuarterTo
toClockSegment n
| n <= 15 = Past n
| n <= 30 = ToHalf (30-n)
| n <= 45 = PastHalf (n-30)
| otherwise = To (60-n)
-- | Convert the given number of hours to the corresponding 'DayPart'.
toDayPart
:: Int -- ^ The given number of hours.
-> DayPart -- ^ The corresponding 'DayPart'.
toDayPart n
| n <= 5 = Night
| n <= 11 = Morning
| n <= 17 = Afternoon
| otherwise = Evening
-- | Convert the given number of hours to the corresponding 'DaySegment'.
toDaySegment
:: Int -- ^ The given number of hours.
-> DaySegment -- ^ The corresponding 'DaySegment'.
toDaySegment n = DaySegment (toDayPart n) (hourCorrection n)
-- | Correct the hour to a 12 number segment.
-- The input can be any Int number, whereas the
-- result will be in the @1 .. 12@ range.
hourCorrection
:: Int -- ^ The value for the number of hours.
-> Int -- ^ The hours in the @1 .. 12@ range.
hourCorrection h = ((h - 1) `mod` 12) + 1
instance Default NumberType where
def = Cardinal
-- | A type class used for num to word algorithms. It maps an 'Integral' type
-- @i@ to 'Text'.
class NumToWord a where
-- | Convert the given number to a 'Text' object that is the given number in
-- words in /cardinal/ form.
toCardinal :: Integral i
=> a -- ^ The conversion algorithm that transforms the number into words.
-> i -- ^ The number to transform into a /cardinal/ form.
-> Text -- ^ The number in words in a /cardinal/ form.
toCardinal = toWords Cardinal
-- | Convert the given number to a 'Text' object that is the given number in
-- words in /cardinal/ form.
toOrdinal :: Integral i
=> a -- ^ The conversion algorithm that transforms the number into words.
-> i -- ^ The number to transform into a /ordinal/ form.
-> Text -- ^ The number in words in a /ordinal/ form.
toOrdinal = toWords Ordinal
-- | Convert the given number to a 'Text' object that is the given number
-- in words in /short cardinal/ form.
toShortOrdinal :: Integral i
=> a -- ^ The conversion algorithm that transforms the number into words.
-> i -- ^ The number to transform into a /ordinal/ form.
-> Text -- ^ The number in words in a /ordinal/ form.
toShortOrdinal = toWords Ordinal
-- | Convert the given number to a 'Text' object that is the given number in
-- words in the given 'NumberType'.
toWords :: Integral i
=> NumberType -- ^ The given format to convert the number to.
-> a -- ^ The conversion algorithm that transforms the number into words.
-> i -- ^ The number to transform into the given form.
-> Text -- ^ The number in words in the given form.
toWords Cardinal = toCardinal
toWords Ordinal = toOrdinal
toWords ShortOrdinal = toShortOrdinal
-- | Convert the given time of the day to text describing that time.
toTimeText
:: a -- ^ The conversion algorithm to transform numbers into words.
-> TimeOfDay -- ^ The time of the day to convert to words.
-> Text -- ^ The time as /text/.
toTimeText gen (TimeOfDay h m _) = toTimeText' gen h m
-- | Convert the given hours and minutes to text that describes the time.
toTimeText'
:: a -- ^ The conversion algorithm to transform numbers into words.
-> Int -- ^ The number of hours, between 0 and 23 (both inclusive)
-> Int -- ^ The number of minutes, beween 0 and 59 (both inclusive)
-> Text -- ^ The time as /text/.
toTimeText' gen h m = toTimeText gen (TimeOfDay h m 0)
{-# MINIMAL ((toCardinal, toOrdinal, toShortOrdinal) | toWords), (toTimeText | toTimeText') #-}
-- | Convert the current time in the given 'TimeZone' to the time in words with the given 'NumToWord'
-- algorithm.
currentTimeText :: NumToWord a
=> TimeZone -- ^ The given 'TimeZone'.
-> a -- ^ The 'NumToWord' algorithm that converts time to words.
-> IO Text -- ^ An 'IO' that will generate a 'Text' object that describes the current time in words.
currentTimeText tz alg = toTimeText alg . snd . utcToLocalTimeOfDay tz . timeToTimeOfDay . utctDayTime <$> getCurrentTime
-- | Convert the current time to the time in words with the given 'NumToWord'
-- algorithm as UTC time.
currentTimeText' :: NumToWord a
=> a -- ^ The 'NumToWord' algorithm that converts time to words.
-> IO Text -- ^ An 'IO' that will generate a 'Text' object that describes the current time in words.
currentTimeText' alg = toTimeText alg . timeToTimeOfDay . utctDayTime <$> getCurrentTime
-- | A type class used to split a value, based on the name of a number in a
-- specific language. The value that is used to split, is often, depending on
-- the language, the largest value smaller than the given number.
class ValueSplit a where
-- | A function that takes an 'Integral' value, and based on the object
-- splits it with a value and the name of the number in a specific language.
valueSplit :: a -> FreeValueSplitter