timeline

Requirements

The timeline program outputs a timeline string on stdout that starts with a date in ISO 8601 format, followed by a date-dependent string. Four timeline string variants are possible, depending on the date:

A date before the start of my partial retirement

For a date before the start of my partial retirement, that is, before 2019-02-01, the timeline string looks like the string in the following example:

2019-01-31: My partial retirement has not yet started.

A date during the active phase of my partial retirement

For a date in the active phase of my partial retirement, that is, in the range 2019-02-01 .. 2021-03-31, the timeline string looks like the string in the following example:

2019-11-05: 790|------------------512~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|0

The overall size of the timeline string is 70 characters.

The date-dependent string starts with 790|, followed by 70 - 18 = 52 dash (-), tilde (~), or digit (0 .. 9) characters, and ends with |0, where the first dash / tilde (nearby 790|) marks the date 2019-01-31, and the last dash / tilde (nearby |0) marks the date 2021-03-31.

The current number of days left (related to the current date), up to the date 2021-03-31, in other words, the difference in days between the date 2021-03-31 and the current date, replaces the appropriate number of dashes (three for a three-digit number) at the “true to scale” position inside the date-dependent string.

Dash characters are used to the left of the current number of days left, whereas tilde characters are used to the right of the current number of days left.

A date during the passive phase of my partial retirement

For a date in the passive phase of my partial retirement, that is, in the range 2021-04-01 .. 2023-05-31, the timeline string looks like the string in the following example:

2021-08-06: 0|~~~~~~~128------------------------------------------|791

The overall size of the timeline string is also 70 characters.

The date-dependent string starts with 0|, followed by 70 - 18 = 52 tilde (~), dash (-), or digit (0 .. 9) characters, and ends with |791, where the first tilde / dash (nearby 0|) marks the date 2021-03-31, and the last tilde / dash (nearby |791) marks the date 2023-05-31.

The current number of days passed (related to the current date), starting at the date 2021-03-31, in other words, the difference in days between the current date and the date 2021-03-31, replaces the appropriate number of tilde characters (three for a three-digit number) at the “true to scale” position inside the date-dependent string.

Tilde characters are used to the left of the current number of days passed, whereas dash characters are used to the right of the current number of days passed.

A date after the start of my retirement

For a date after the start of my retirement, that is, at 2023-06-01 or later, the timeline string looks like the string in the following example:

2023-06-01: R.E.D.

Implementation in Haskell

For some time passed, functional programming is my passion. Therefore, I wrote the following small Haskell program to create the timeline string:

-- timeline.hs


import Data.Monoid
import Data.Time
import System.Environment


timeLineSize :: Integer
timeLineSize = 70

startDateActive, startDatePassive, startDateRetired  :: (Integer, Int, Int)
startDateActive  = (2019, 02, 01)
startDatePassive = (2021, 04, 01)
startDateRetired = (2023, 06, 01)


startDayActive, startDayPassive, startDayRetired:: Day
startDayActive  = fromGregorian' startDateActive
startDayPassive = fromGregorian' startDatePassive
startDayRetired = fromGregorian' startDateRetired

daysActive, daysPassive :: Integer
daysActive  = startDayPassive `diffDays` startDayActive
daysPassive = startDayRetired `diffDays` startDayPassive

prefixActive, suffixActive, prefixPassive, suffixPassive :: String
prefixActive  = show daysActive ++ "|"
suffixActive  = "|0"
prefixPassive = "0|"
suffixPassive = "|" ++ show daysPassive

prefixSuffixLengthActive, prefixSuffixLengthPassive :: Integer
prefixSuffixLengthActive  = fromIntegral $ length $ currentDate startDayActive  ++ prefixActive  ++ suffixActive
prefixSuffixLengthPassive = fromIntegral $ length $ currentDate startDayPassive ++ prefixPassive ++ suffixPassive


fromGregorian' :: (Integer, Int, Int) -> Day
fromGregorian' (year, month, day) = fromGregorian year month day

scale :: Integer -> Integer -> Integer -> Integer
scale numerator denominator value =
  (floor :: Double -> Integer) $ fromIntegral (value * numerator) / fromIntegral denominator

mapper :: Integer -> Integer -> Integer -> String
mapper dayDiff scaled x
  | scaled < x = "-"
  | scaled > x = "~"
  | otherwise = show dayDiff


currentDate :: Day -> String
currentDate = (++ ": ") . showGregorian

beforeActive :: Day -> String
beforeActive day
  | day < startDayActive = "My partial retirement has not yet started."
  | otherwise = ""

duringActive :: Day -> String
duringActive day
  | day >= startDayActive && day < startDayPassive = prefixActive ++ (line >>= mapper') ++ suffixActive
  | otherwise = ""
  where
    daysToEndActive = startDayPassive `diffDays` day - 1
    start = timeLineSize - prefixSuffixLengthActive - fromIntegral (length $ show daysToEndActive)
    line = [start,start - 1 .. 0]
    mapper' :: Integer -> String
    mapper' = mapper daysToEndActive $ scale start daysActive daysToEndActive

duringPassive :: Day -> String
duringPassive day
  | day >= startDayPassive && day < startDayRetired = prefixPassive ++ (line >>= mapper') ++ suffixPassive
  | otherwise = ""
  where
    dayInPassive = day `diffDays` startDayPassive + 1
    end = timeLineSize - prefixSuffixLengthPassive - fromIntegral (length $ show dayInPassive)
    line = [0 .. end]
    mapper' :: Integer -> String
    mapper' = mapper dayInPassive $ scale end daysPassive dayInPassive

duringRetired :: Day -> String
duringRetired day
  | day >= startDayRetired = "R.E.D."
  | otherwise = ""


timeLine :: Day -> String
timeLine = currentDate <> beforeActive <> duringActive <> duringPassive <> duringRetired

timeLineIO :: IO String
timeLineIO = timeLine . localDay . zonedTimeToLocalTime <$> getZonedTime


main :: IO ()
main = do
  args <- getArgs
  if null args || null (head args)
    then timeLineIO >>= putStrLn
    else mapM_ (putStrLn . timeLine) [addDays (-3) startDayActive .. addDays 2 startDayRetired]


-- EOF

There is no doubt that the implementation is possible in any language. But functional programming, especially Haskell, offers some cool stuff that I want to mention here.

Timeline string size, start dates of the (partial) retirement phases

timeLineSize defines the overall size of the timeline string, mentioned above, and is of type Integer:

timeLineSize :: Integer
timeLineSize = 70

startDateActive, startDatePassive, and startDateRetired define the start dates of the three phases of my (partial) retirement. Each variable is of type triple of Integer, Int, Int, where the first triple element is the year, the second element is the month, and the third element is the day:

startDateActive, startDatePassive, startDateRetired  :: (Integer, Int, Int)
startDateActive  = (2019, 02, 01)
startDatePassive = (2021, 04, 01)
startDateRetired = (2023, 06, 01)

Haskell variables are variables in the math sense, i.e. they are immutable. Haskell as a statically-typed language is very nitpicking concerning types. But fortunately, due to type inference, the compiler can deduce a lot of types automatically (cf. auto in C++).

Some further useful values

The variables startDayActive, startDayPassive, and startDayRetired are all of type Day, where Day is the Modified Julian Day as a standard count of days, with zero being the day 1858-11-17:

startDayActive, startDayPassive, startDayRetired:: Day
startDayActive  = fromGregorian' startDateActive
startDayPassive = fromGregorian' startDatePassive
startDayRetired = fromGregorian' startDateRetired

These variables are created by function fromGregorian'. fromGregorian' is a function of type (Integer, Int, Int) → Day, i.e. it expects a triple of Integer, Int, Int and returns a Day:

fromGregorian' :: (Integer, Int, Int) -> Day
fromGregorian' (year, month, day) = fromGregorian year month day

The prime (') does not make the function or the function name special. fromGregorian' is just a different function than fromGregorian (with a different name).

The function fromGregorian' is called with a triple, e.g. startDateActive. Pattern matching is used to assign the first value of the triple to the variable year, the second value to month, and the third value to day. Now, fromGregorian'calls the system library function fromGregorian, which expects these three parameters as input.

The variable daysActive defines the number of days during the active phase of my partial retirement (790 days), whereas the variable daysPassive defines the number of days during the passive phase (791 days):

daysActive, daysPassive :: Integer
daysActive  = startDayPassive `diffDays` startDayActive
daysPassive = startDayRetired `diffDays` startDayPassive

The backticks make the system library function diffDays an infix operator. It could also be written as

daysActive  = diffDays startDayPassive startDayActive
daysPassive = diffDays startDayRetired startDayPassive

But written as infix operator it is clear in the first place, which value shall be subtracted from which value.

The String variable prefixActive is set to 790| by concatenating the string representation of daysActive (using function show) and the character |. The String variable suffixPassive is set to |791 by concatenating the character | and the string representation of daysPassive:

prefixActive, suffixActive, prefixPassive, suffixPassive :: String
prefixActive  = show daysActive ++ "|"
suffixActive  = "|0"
prefixPassive = "0|"
suffixPassive = "|" ++ show daysPassive

The Integer variables prefixSuffixLengthActive and prefixSuffixLengthPassive both have a value of 18 and are calculated as follows:

prefixSuffixLengthActive, prefixSuffixLengthPassive :: Integer
prefixSuffixLengthActive  = fromIntegral $ length $ currentDate startDayActive  ++ prefixActive  ++ suffixActive
prefixSuffixLengthPassive = fromIntegral $ length $ currentDate startDayPassive ++ prefixPassive ++ suffixPassive

the $ sign is a function application that is used to avoid additional parenthesizes. You could also write for example:

prefixSuffixLengthActive  = fromIntegral (length (currentDate startDayActive ++ prefixActive ++ suffixActive))

The infix operator $ is right associate with lowest precedence. In contrast, normal function application (via white space) is left associative with highest precedence.

The function currentDate expects a Day (Modified Julian Day) value, and returns the related date in ISO 8601 format, concatenated with a colon and a space character:

currentDate :: Day -> String
currentDate = (++ ": ") . showGregorian

The function is defined in pointfree notation, that is, with no function arguments. The system library function showGregorian expects a Day value and returns a string representation of this value.

++ as an infix operator can also be written like an ordinary function, using parenthesizes: (++). This is a function that expects two (String) parameters. (++ ": ") is a function with one parameter (here: ": ") already applied. This partial application leads to a function that expects one (String) parameter.

The function currentDate is a function composition of showGregorian and (++ ": ") and is realized with the dot operator: (++ ": ") . showGregorian. . is read as after. The result is a function (namely currentDate) that expects a Day and returns a String. It could also be written as

currentDate :: Day -> String
currentDate day = showGregorian day ++ ": "

As a consequence, the variables prefixSuffixLengthActive and prefixSuffixLengthPassive get the value 18 as follows:

2019-11-05: 790|------------------512~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|0
\__________/\__/                                                    \/
    12       4                                                      2

2021-08-06: 0|~~~~~~~128------------------------------------------|791
\__________/\/                                                    \__/
    12      2                                                      4

Helper functions

The function scale expects a numerator, a denominator, and a value, and returns value * (numerator / denominator) that is, it scales a given value proportionally:

scale :: Integer -> Integer -> Integer -> Integer
scale numerator denominator value =
  (floor :: Double -> Integer) $ fromIntegral (value * numerator) / fromIntegral denominator

The function mapper expects the Integer values dayDiff, scaled, and x, and returns a String, which is either a dash, a tilde, or the string representation of dayDiff.

mapper :: Integer -> Integer -> Integer -> String
mapper dayDiff scaled x
  | scaled < x = "-"
  | scaled > x = "~"
  | otherwise = show dayDiff

The function uses guards (|) for comparing scaled and x.

The functions beforeActive, duringActive, duringPassive, and duringRetired expect a Day value each and return a string, depending on the given Day value.

Each function checks the given Day for being in the expected range, using guards (|). If Day is in the expected range, the function outputs a non-empty string, otherwise an empty string. For the function beforeActive this is simple:

beforeActive :: Day -> String
beforeActive day
  | day < startDayActive = "My partial retirement has not yet started."
  | otherwise = ""

Function duringRetired has an easy job, too:

duringRetired :: Day -> String
duringRetired day
  | day >= startDayRetired = "R.E.D."
  | otherwise = ""

The function duringActive has to create a date-dependent string, as described in section Requirements:

duringActive :: Day -> String
duringActive day
  | day >= startDayActive && day < startDayPassive = prefixActive ++ (line >>= mapper') ++ suffixActive
  | otherwise = ""
  where
    daysToEndActive = startDayPassive `diffDays` day - 1
    start = timeLineSize - prefixSuffixLengthActive - fromIntegral (length $ show daysToEndActive)
    line = [start,start - 1 .. 0]
    mapper' :: Integer -> String
    mapper' = mapper daysToEndActive $ scale start daysActive daysToEndActive

line is a list of Integer values. mapper' is a nested function, which is mapped over the list, that is, it is called repeatedly for one list element after the other, and returns a string that is either a dash (-), a tilde (~), or the string representation of daysToEndActive.

The mapper' function is called in the expression line >>= mapper'. >>= is the bind operator, which is defined for a monad and therefore also for line, which is a list. (Lists are monads.)

When the bind operator is applied to line and mapper', it executes the function mapper' for each element of line and creates a string (not a list of strings) as a result. As an alternative, we could also use the function map as follows:

map mapper' line

Watch that mapper' is of type Integer -> String; therefore map creates a list of strings (instead of a string). We could use concat to “flatten” the list of strings to one string. As an alternative, the function concatMap does the mapping, followed by the “flattening”, in one step.

map is a list-specific implementation of the function fmap, which is defined for functors. (Lists are functors.) concat is the list-specific implementation of the function join, which is also defined for functors.

The function composition join . fmap is exactly what the bind operator does for monads. Therefore, the following expressions are all equivalent for lists, since lists are functors and monads:

concatMap mapper' line
concat (map mapper' line)
join (fmap mapper' line)  -- needs 'import Control.Monad'
(line >>= mapper')

Look, how the function mapper' is defined:

mapper' :: Integer -> String
mapper' = mapper daysToEndActive normalized

mapper' calls function mapper with two arguments instead of three (partial application). The result of this calling is a function that expects one argument. This function (mapper') is used as an argument of the higher order function (>>=).

Like function duringActive, the function duringPassive has also to create a date-dependent string, as described in section Requirements, but in a slightly different way:

duringPassive :: Day -> String
duringPassive day
  | day >= startDayPassive && day < startDayRetired = prefixPassive ++ (line >>= mapper') ++ suffixPassive
  | otherwise = ""
  where
    dayInPassive = day `diffDays` startDayPassive + 1
    end = timeLineSize - prefixSuffixLengthPassive - fromIntegral (length $ show dayInPassive)
    line = [0 .. end]
    mapper' :: Integer -> String
    mapper' = mapper dayInPassive $ scale end daysPassive dayInPassive

Some parameters and the nested function mapper' are different.

Output a timeline string for a given MJD

The function timeLine expects a Day value and returns the timeline string, related to this Day value:

timeLine :: Day -> String
timeLine = currentDate <> beforeActive <> duringActive <> duringPassive <> duringRetired

<> is an infix operator, which can be replaced with the function mappend. The function mappend (and therefore the infix operator <>) is implemented for monoids. Since Strings are monoids, mappend is also implemented for strings in a way that it concatenates them.

Did you notice that the functions currentDate, beforeActive, duringActive, duringPassive, and duringRetired all expect a Day value and return a String? currentDate always returns a non-empty string, whereas only one of the remaining functions returns a non-empty string, too. This allows us to “chain” the functions with the <> operator, which makes timeLine a “one liner”.

The function timeLineIO returns the (timeline) string that is related to the current date:

timeLineIO :: IO String
timeLineIO = timeLine . localDay . zonedTimeToLocalTime <$> getZonedTime

getZonedTime is of type IO String, where IO is a monad, that is, getZonedTime delivers the current local time, together with a time zone, as IO ZonedTime (not ZonedTime). The function is not pure, i.e. it does not always return the same result, when called. IO is a computational context that cannot be left. (This would contaminate our pure world with impure stuff–which is absolute no-go.) This is the reason, why timeLineIO must also be of type IO String (not just String). timeLineIO holds an IO String (not a String).

As mentioned, getZonedTime returns a monad. Since all monads are also functors, the function fmap (here as infix operator <$>) is implemented also for the result of getZonedTime. This allows us to apply a function to the computational context (the monad), where the function does not know anything about this computational context.

Here, we do not apply just one function, but a function composition: timeLine . localDay . zonedTimeToLocalTime. The composed function is a function that expects a ZonedTime. fmap lifts this composed function to the computational context and therefore calls the composed function with ZonedTime, which is what we need.

The composed function converts the ZonedTime to the LocalTime, gets the Day value from this LocalTime, and creates the timeline, related to this Day value. The resulting String value is “put back” to the computational context by fmap, that is, the final result is an IO String (as desired).

The current local time is used here instead of UTC, because I want to get a new (decreased or increased) number of days left exactly at midnight at my location (Germany) with daylight saving time taken into account. If UTC shall be used instead, the function timeLineIO looks slightly different:

timeLineIO :: IO String
timeLineIO = timeLine . utctDay <$> getCurrentTime

getCurrentTime returns an IO monad as well, but it delivers the current UTC time as IO UTCTime. The composed function converts UTCTime to a Day value and creates the timeline, as described above.

The main function

The main function outputs the timeline, related to the current date, or the timelines for all dates between startDayActive - 3 and startDayRetired + 2 (depending on the given command line):

main :: IO ()
main = do
  args <- getArgs
  if null args || null (head args)
    then timeLineIO >>= putStrLn
    else mapM_ (putStrLn . timeLine) [addDays (-3) startDayActive .. addDays 2 startDayRetired]

Since such an output is just a side effect without reasonable return value, it is also called performing an action.

The bind operator >>=, which is implemented for a monad, gets an IO String here (namely the result of timeLineIO), removes the computational context (the IO), and calls putStrLn. This function expects (and gets) a String value and outputs it on stdout (more precisely: it creates an IO action that is then performed by main):

timeLineIO >>= putStrLn

For no command line argument set, the output is the timeline, related to the current date. But for any (non-empty) command line argument, in

mapM_ (putStrLn . timeLine) [addDays (-3) startDayActive .. addDays 2 startDayRetired]

the function mapM_ maps the composition of the functions putStrLn after timeLine over all dates between startDayActive - 3 and startDayRetired + 2.

The do notation is just syntactical sugar for the bind operator. Therefore, the following expression has the same effect as the do notation:

main :: IO ()
main =
  getArgs >>= \args ->
    if null args || null (head args)
      then timeLineIO >>= putStrLn
      else mapM_ (putStrLn . timeLine) [addDays (-3) startDayActive .. addDays 2 startDayRetired]

getArgs returns IO [String]. The bind operator removes the computational context and calls the lambda abstraction \args -> ... with [String] (a list of Strings). This anonymous function checks, if the list of strings is empty (i.e. no command line arguments are given) or if the first list element is empty. The latter is necessary to catch callings like timeLine "" properly.

Watch that the term null (head args) is executed only, if the term null args is False. This short circuiting prevents head from being executed for an empty list. Since Haskell is a lazy language, you get short circuiting for free. In most languages, ||, &&, and similar operators must be built specially into the language in order for them to short circuit evaluation.

As a consequence, main executes timeLineIO for no (non-empty) command argument given, otherwise mapM_.

Conclusion

Phew! I can’t believe, how much can be told about such a small implementation! But all of this is a rock-solid foundation for a bunch of similar requirements. (Fun fact: The Markdown source of this documentation is converted to HTML by Pandoc, which is written in Haskell.)

Wu