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:
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: Days left to start of partial retirement ............... 1
The overall size of the timeline string is 70 characters.
The date-dependent string starts with the text
Days left to start of partial retirement, followed by dots,
and ends with the number of days left (related to the current date), up
to the date 2019-02-01, in other words, the difference in days
between the date 2019-02-01 and the current date.
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.
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 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.
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: RED .................................................... 1
The overall size of the timeline string is 70 characters.
The date-dependent string starts with the text RED,
followed by dots, and ends with the number of days passed (related to
the current date), starting at the date 2023-05-31, in other
words, the difference in days between the current date and the date
2023-05-31.
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.Time
import System.Environment
timeLineSize :: Int
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 :: Int
daysActive = fromInteger $ startDayPassive `diffDays` startDayActive
daysPassive = fromInteger $ startDayRetired `diffDays` startDayPassive
prefixActive, suffixActive, prefixPassive, suffixPassive :: String
prefixActive = show daysActive <> "|"
suffixActive = "|0"
prefixPassive = "0|"
suffixPassive = "|" <> show daysPassive
prefixBeforeActive, prefixRetired :: String
prefixBeforeActive = "Days left to start of partial retirement "
prefixRetired = "RED "
prefixSuffixLengthActive, prefixSuffixLengthPassive :: Int
prefixSuffixLengthActive = length $ mconcat [currentDate startDayActive, prefixActive, suffixActive]
prefixSuffixLengthPassive = length $ mconcat [currentDate startDayPassive, prefixPassive, suffixPassive]
prefixLengthBeforeActive, prefixLengthRetired :: Int
prefixLengthBeforeActive = length $ currentDate startDayActive <> prefixBeforeActive
prefixLengthRetired = length $ currentDate startDayPassive <> prefixRetired
fromGregorian' :: (Integer, Int, Int) -> Day
fromGregorian' (year, month, day) = fromGregorian year month day
scale :: Int -> Int -> Int -> Int
scale numerator denominator value
| denominator == 0 = 0
| otherwise = value * numerator `div` denominator
mapper :: Int -> Int -> Int -> String
mapper dayDiff scaled x
| scaled < x = "-"
| scaled > x = "~"
| otherwise = show dayDiff
currentDate :: Day -> String
currentDate = (<> ": ") . showGregorian
beforeActive :: Day -> String
beforeActive day
| day < startDayActive = mconcat [prefixBeforeActive, replicate numDots '.', " ", show daysToStartActive]
| otherwise = ""
where
daysToStartActive = startDayActive `diffDays` day
numDots = timeLineSize - sum [prefixLengthBeforeActive, length (show daysToStartActive), 1]
duringActive :: Day -> String
duringActive day
| day >= startDayActive && day < startDayPassive = mconcat [prefixActive, line >>= mapper', suffixActive]
| otherwise = ""
where
daysToEndActive = fromInteger $ startDayPassive `diffDays` day - 1
start = timeLineSize - prefixSuffixLengthActive - length (show daysToEndActive)
line = [start,start - 1 .. 0]
mapper' :: Int -> String
mapper' = mapper daysToEndActive $ scale start daysActive daysToEndActive
duringPassive :: Day -> String
duringPassive day
| day >= startDayPassive && day < startDayRetired = mconcat [prefixPassive, line >>= mapper', suffixPassive]
| otherwise = ""
where
dayInPassive = fromInteger $ day `diffDays` startDayPassive + 1
end = timeLineSize - prefixSuffixLengthPassive - length (show dayInPassive)
line = [0 .. end]
mapper' :: Int -> String
mapper' = mapper dayInPassive $ scale end daysPassive dayInPassive
duringRetired :: Day -> String
duringRetired day
| day >= startDayRetired = mconcat [prefixRetired, replicate numDots '.', " ", show dayInRetired]
| otherwise = ""
where
dayInRetired = day `diffDays` startDayRetired + 1
numDots = timeLineSize - sum [prefixLengthRetired, length (show dayInRetired), 1]
timeLine :: Day -> String
timeLine = mconcat [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]
-- EOFThere 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.
timeLineSize defines the overall size of the timeline
string, mentioned above, and is of type Int:
timeLineSize :: Int
timeLineSize = 70startDateActive, 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++).
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' startDateRetiredThese 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 dayThe 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 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 :: Int
daysActive = fromInteger $ startDayPassive `diffDays` startDayActive
daysPassive = fromInteger $ startDayRetired `diffDays` startDayPassiveThe backticks make the library function diffDays an
infix operator. It could also be written as
daysActive, daysPassive :: Int
daysActive = fromInteger $ diffDays startDayPassive startDayActive
daysPassive = fromInteger $ diffDays startDayRetired startDayPassiveBut written as infix operator it is clear in the first place, which value shall be subtracted from which value.
The function fromInteger converts the result of
diffDays, which is of type Integer, to
Int. Normally, we have to be careful with this, since the
conversion might result in 0. But in our case the day
difference values are always small enough to fit in the Int
type.
The $ sign is a function application that is used to
avoid additional parenthesizes. You could also write:
daysActive, daysPassive :: Int
daysActive = fromInteger (startDayPassive `diffDays` startDayActive)
daysPassive = fromInteger (startDayRetired `diffDays` startDayPassive)The infix operator $ is right associate with lowest
precedence. In contrast, normal function application (via white space)
is left associative with highest precedence.
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 daysPassiveInstead of <>, we could also use ++.
But the latter is only defined for lists (String is just a type
“alias” for [Char]). There is a better (that is, more
performant alternative) available for String: Text (in
module Data.Text and Data.Text.Lazy). ++
is not defined for Text, but <> is, because
Text (as well as lists) is a semigroup.
If it is necessary to switch to Text, it is easy, if we use
<> in the first place.
The String variables prefixBeforeActive and
prefixRetired are simple:
prefixBeforeActive, prefixRetired :: String
prefixBeforeActive = "Days left to start of partial retirement "
prefixRetired = "RED "The Int variables prefixSuffixLengthActive and
prefixSuffixLengthPassive both have a value of
18 and are calculated as follows:
prefixSuffixLengthActive, prefixSuffixLengthPassive :: Int
prefixSuffixLengthActive = length $ mconcat [currentDate startDayActive, prefixActive, suffixActive]
prefixSuffixLengthPassive = length $ mconcat [currentDate startDayPassive, prefixPassive, suffixPassive]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 = (<> ": ") . showGregorianThe function is defined in pointfree
notation, that is, with no function arguments. The 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 (semigroup, here:
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
Since String and Text are also monoids, we can use
the related concatenation function mconcat with a list of
String (or Text) instead of using
<> repeatedly:
mconcat [currentDate startDayActive, prefixActive, suffixActive]is equivalent to
currentDate startDayActive <> prefixActive <> suffixActiveThe Int variables prefixLengthBeforeActive and
prefixLengthRetired get their values as follows (watch the
trailing blanks):
2019-01-31: Days left to start of partial retirement
\__________/\_______________________________________/
12 41
2023-06-01: RED
\__________/\__/
12 4
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 :: Int -> Int -> Int -> Int
scale numerator denominator value
| denominator == 0 = 0
| otherwise = value * numerator `div` denominatorAlthough not necessary here (because the denominator cannot be 0), it is better practice to prevent a possible exception due to division by zero in the first place. One solution could be to return Maybe Int instead of Int. Our simple solution here is just returning 0 for a denominator set to 0.
The function uses guards
(|) for checking the 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 :: Int -> Int -> Int -> String
mapper dayDiff scaled x
| scaled < x = "-"
| scaled > x = "~"
| otherwise = show dayDiffThe 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 (date-dependent)
string, otherwise an empty string. For the function
beforeActive this is rather simple:
beforeActive :: Day -> String
beforeActive day
| day < startDayActive = mconcat [prefixBeforeActive, replicate numDots '.', " ", show daysToStartActive]
| otherwise = ""
where
daysToStartActive = startDayActive `diffDays` day
numDots = timeLineSize - sum [prefixLengthBeforeActive, length (show daysToStartActive), 1]The job of function duringRetired is not so difficult as
well:
duringRetired :: Day -> String
duringRetired day
| day >= startDayRetired = mconcat [prefixRetired, replicate numDots '.', " ", show dayInRetired]
| otherwise = ""
where
dayInRetired = day `diffDays` startDayRetired + 1
numDots = timeLineSize - sum [prefixLengthRetired, length (show dayInRetired), 1]The function duringActive has to create a date-dependent
string as follows:
duringActive :: Day -> String
duringActive day
| day >= startDayActive && day < startDayPassive = mconcat [prefixActive, line >>= mapper', suffixActive]
| otherwise = ""
where
daysToEndActive = fromInteger $ startDayPassive `diffDays` day - 1
start = timeLineSize - prefixSuffixLengthActive - length (show daysToEndActive)
line = [start,start - 1 .. 0]
mapper' :: Int -> String
mapper' = mapper daysToEndActive $ scale start daysActive daysToEndActiveline is a list of Int 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' lineWatch that mapper' is of type Int -> 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' :: Int -> String
mapper' = mapper daysToEndActive $ scale start daysActive daysToEndActivemapper' 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 = mconcat [prefixPassive, line >>= mapper', suffixPassive]
| otherwise = ""
where
dayInPassive = fromInteger $ day `diffDays` startDayPassive + 1
end = timeLineSize - prefixSuffixLengthPassive - length (show dayInPassive)
line = [0 .. end]
mapper' :: Int -> String
mapper' = mapper dayInPassive $ scale end daysPassive dayInPassiveSome parameters and the nested function mapper' are
different.
The function timeLine expects a Day value and
returns the timeline string, related to this Day value:
timeLine :: Day -> String
timeLine = mconcat [currentDate, beforeActive, duringActive, duringPassive, duringRetired]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. These
functions are monoids. This property allows us to use the
mconcat function, 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 <$> getZonedTimegetZonedTime 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). IO is a computational context
that cannot be left. (This would contaminate our pure world with impure
stuff–which is a no-no.) 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 <$> getCurrentTimegetCurrentTime 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.
main functionThe 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 the Haskell runtime system):
timeLineIO >>= putStrLnFor 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 syntactic sugar for the bind operator (among other things). 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_.
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
