Search code examples
stringtimesmalltalk

Time formatting (HH:MM:SS) in any Smalltalk dialect


I have three integer values, say

h := 3.
m := 19.
s := 8.

I would like to produce the string '03:19:08'. I know how to turn a number into a string, and even pad it with a zero if necessary. So as a first pass I wrote this absolutely horrific code:

h < 10 ifTrue: [hs := '0', (h asString)] ifFalse: [hs := h asString].
m < 10 ifTrue: [ms := '0', (m asString)] ifFalse: [ms := m asString].
s < 10 ifTrue: [ss := '0', (s asString)] ifFalse: [ss := s asString].
Transcript show: hs, ':', ms, ':', ss.
Transcript nl.

Now obviously I need to clean this up and so was wondering, among other things what the most idiomatic Smalltalk approach would be here. Could it be something like (not legal Smalltalk obviously):

aCollectionWithHMS each [c | padWithZero] join ':'

I found a discussion on streams with a print method taking a separatedBy argument but wouldn't there be a simpler way to do things just with strings?

Or perhaps there is a more elegant way to pad the three components and then I could just return hs, ':', ms, ':', ss ?

Or, is there an interface to POSIX time formatting (or something similar) common to all Smalltalks? I know GNU Smalltalk can link to C but this is way too much overkill for this simple problem IMHO.

EDIT

I got a little closer:

z := {h . m . s} collect: [:c | c < 10 ifTrue: ['0', c asString] ifFalse: [c asString]].
(Transcript show: ((z at: 1), ':',  (z at: 2), ':', (z at: 3))) nl.

But the direct access of collection elements makes me sad. I found a page documenting the joining method asStringWith but that method is unsupported, it seems in GNU Smalltalk.


Solution

  • Here is a way to do this in Pharo:

    String streamContents: [:stream |
      {h.m.s}
        do: [:token | token printOn: stream base: 10 nDigits: 2]
        separatedBy: [stream nextPut: $:]]
    

    Explanation:

    1. The streamContents: message answers with the contents of the WriteStream represented by the formal block argument stream.

    2. The do:separatedBy: message enumerates the tokens h, m and s evaluating the do: block for each of them and inserting the evaluation of the second block between consecutive tokens.

    3. The printOn:base:nDigits: message dumps on the stream the base 10 representation of the token padded to 2 digits.

    If the dialect you are using doesn't have the printOn:base:nDigits: method (or any appropriate variation of it), you can do the following:

    String streamContents: [:stream |
      {h.m.s}
        do: [:token |
          token < 10 ifTrue: [stream nextPut: $0].
          stream nextPutAll: token asString]
        separatedBy: [stream nextPut: $:]]
    

    Finally, if you think you will be using this a lot, I would recommend adding the message hhmmss to Time (instance side), implemented as above with self hours instead of h, etc. Then it would be a matter of sending

    (Time hour: h minute: m second: s) hhmmss
    

    assuming you have these three quantities instead of a Time object, which would be unusual. Otherwise, you would only need something like

    aTime hhmmss
    

    ADDENDUM

    Here is another way that will work on any dialect:

    {h.m.s}
      inject: ''
      into: [:r :t | | pad colon |
        pad := t < 10 ifTrue: ['0'] ifFalse: [''].
        colon := r isEmpty ifTrue: [''] ifFalse: [':'].
        r , colon, pad, t asString]
    

    The inject:into: method builds its result from the inject: argument (the empty String in this case) and keeps replacing the formal block argument r with the value of the previous iteration. The second formal argument t is replaced with the corresponding element of each iteration.


    ADDENDUM 2

    time := '00:00:00' copy.
    {h asString. m asString. s asString} withIndexDo: [:t :i |
      time at: i - 1 * 3 + 2 put: t last.
      t size = 2 ifTrue: [time at: i - 1 * 3 + 1 put: t first]].
    ^time
    

    The copy is necessary to make sure that the literal is not modified.