Ray Toal
Ray Toal

Reputation: 88378

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.

Upvotes: 1

Views: 191

Answers (1)

Leandro Caniglia
Leandro Caniglia

Reputation: 14858

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.

Upvotes: 3

Related Questions