ordinal-0.6.0.0: src/Text/Numerals/Class.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE 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
{ -- | The optional division part. 'Nothing' if the division is equal to one.
segmentDivision :: MNumberSegment i,
-- | The value of the given segment.
segmentValue :: i,
-- | The name of the value of the given segment, in a specific language.
segmentText :: Text,
-- | The optional remainder part. 'Nothing' if the remainder is equal to zero.
segmentRemainder :: MNumberSegment i
}
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/ numbers like one, two, three, etc.
Cardinal
| -- | /Ordinal/ numbers like first, second, third, etc.
Ordinal
| -- | /Short ordinal/ numbers like 1st, 2nd, 3rd, etc.
ShortOrdinal
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 =
-- | The 'ClockSegment' that describes the state of minutes within an hour.
ClockSegment ->
-- | The 'DaySegment' that describes the state of hours within a day.
DaySegment ->
-- | The number of hours.
Int ->
-- | The number of minutes.
Int ->
-- | A 'Text' object that describes the given time.
Text
-- | A data type that describes the state of the minutes within an hour.
data ClockSegment
= -- | The number of minutes is zero.
OClock
| -- | The parameter is the number of minutes past the hour, this is between @1@ and @14@.
Past Int
| -- | It is a quarter past the hour.
QuarterPast
| -- | The parameter is the number of minutes until half, this is between @1@ and @14@.
ToHalf Int
| -- | It is half past an hour.
Half
| -- | The parameter is the number of minutes past half, this is between @1@ and @14@.
PastHalf Int
| -- | It is a quarter to an hour.
QuarterTo
| -- | The parameter is the number of minutes to the next hour, this is between @1@ and @14@.
To Int
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
= -- | It is night, this means that it is between @0:00@ and @5:59@.
Night
| -- | It is morning, this means that it is between @6:00@ and @11:59@.
Morning
| -- | It is afternoon, this means it is between @12:00@ and @17:59@.
Afternoon
| -- | It is evening, this means it is between @18:00@ and @23:59@.
Evening
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
{ -- | The part of the day.
dayPart :: DayPart,
-- | The number of hours, between @1@ and @12@ (both inclusive).
dayHour :: Int
}
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 ::
-- | The number of minutes.
Int ->
-- | The corresponding 'ClockSegment'.
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 ::
-- | The given number of hours.
Int ->
-- | The corresponding 'DayPart'.
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 ::
-- | The given number of hours.
Int ->
-- | The corresponding 'DaySegment'.
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 ::
-- | The value for the number of hours.
Int ->
-- | The hours in the @1 .. 12@ range.
Int
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 =>
-- | The conversion algorithm that transforms the number into words.
a ->
-- | The number to transform into a /cardinal/ form.
i ->
-- | The number in words in a /cardinal/ form.
Text
toCardinal = toWords Cardinal
-- | Convert the given number to a 'Text' object that is the given number in
-- words in /cardinal/ form.
toOrdinal ::
Integral i =>
-- | The conversion algorithm that transforms the number into words.
a ->
-- | The number to transform into a /ordinal/ form.
i ->
-- | The number in words in a /ordinal/ form.
Text
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 =>
-- | The conversion algorithm that transforms the number into words.
a ->
-- | The number to transform into a /ordinal/ form.
i ->
-- | The number in words in a /ordinal/ form.
Text
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 =>
-- | The given format to convert the number to.
NumberType ->
-- | The conversion algorithm that transforms the number into words.
a ->
-- | The number to transform into the given form.
i ->
-- | The number in words in the given form.
Text
toWords Cardinal = toCardinal
toWords Ordinal = toOrdinal
toWords ShortOrdinal = toShortOrdinal
-- | Convert the given time of the day to text describing that time.
toTimeText ::
-- | The conversion algorithm to transform numbers into words.
a ->
-- | The time of the day to convert to words.
TimeOfDay ->
-- | The time as /text/.
Text
toTimeText gen (TimeOfDay h m _) = toTimeText' gen h m
-- | Convert the given hours and minutes to text that describes the time.
toTimeText' ::
-- | The conversion algorithm to transform numbers into words.
a ->
-- | The number of hours, between 0 and 23 (both inclusive)
Int ->
-- | The number of minutes, beween 0 and 59 (both inclusive)
Int ->
-- | The time as /text/.
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 =>
-- | The given 'TimeZone'.
TimeZone ->
-- | The 'NumToWord' algorithm that converts time to words.
a ->
-- | An 'IO' that will generate a 'Text' object that describes the current time in words.
IO Text
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 =>
-- | The 'NumToWord' algorithm that converts time to words.
a ->
-- | An 'IO' that will generate a 'Text' object that describes the current time in words.
IO Text
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