Telephone number in spoken words

33

5

Goal

Write a program or function that translates a numerical telephone number into text that makes it easy to say. When digits are repeated, they should be read as "double n" or "triple n".

Requirements

Input

A string of digits.

  • Assume all characters are digits from 0 to 9.
  • Assume the string contains at least one character.

Output

Words, separated by spaces, of how these digits can be read out loud.

  • Translate digits to words:

    0 "oh"
    1 "one"
    2 "two"
    3 "three"
    4 "four"
    5 "five"
    6 "six"
    7 "seven"
    8 "eight"
    9 "nine"

  • When the same digit is repeated twice in a row, write "double number".

  • When the same digit is repeated thrice in a row, write "triple number".
  • When the same digit is repeated four or more times, write "double number" for the first two digits and evaluate the rest of the string.
  • There is exactly one space character between each word. A single leading or trailing space is acceptable.
  • Output is not case sensitive.

Scoring

Source code with the least bytes.

Test Cases

input        output
-------------------
0123         oh one two three
4554554      four double five four double five four
000          triple oh
00000        double oh triple oh
66667888     double six double six seven triple eight
19999999179  one double nine double nine triple nine one seven nine

Hand-E-Food

Posted 2019-08-13T04:32:58.583

Reputation: 7 912

38Anyone interested in "speech golf" should note that "double six" takes longer to say than "six six". Of all the numerical possibilities here, only "triple seven" saves syllables. – Purple P – 2019-08-13T04:40:19.303

13@Purple P: And as I'm sure you know, 'double-u double-u double-u'>'world wide web'.. – Chas Brown – 2019-08-13T04:43:16.477

11I vote to change that letter to "dub". – Hand-E-Food – 2019-08-13T04:45:11.957

2You should have used non-English/standard digit names for all digits to avoid build-ins… – Adám – 2019-08-13T06:12:19.963

"oh" and not "zero"? – Shaun Bebbers – 2019-08-13T10:20:38.287

So oh and zero are both acceptable? – Shaun Bebbers – 2019-08-13T12:01:09.587

@ShaunBebbers Is there a builtin in your language? ;) – V. Courtois – 2019-08-13T12:29:30.130

@recursive the test cases show 9999999 should be double nine double nine triple nine, not double nine double nine double nine nine as your comment suggests is correct? – 640KB – 2019-08-13T17:33:06.903

I think I misread the spec. It says "When the same digit is repeated four or more times, write "double number" for the first two digits and evaluate the rest of the string." However, I read it as "first two pairs of digits". @640KB: I think you're totally correct. – recursive – 2019-08-13T17:53:33.987

8I know this is only an intellectual exercise, but I have in front of me a gas bill with the number 0800 048 1000, and I would read that as "oh eight hundred oh four eight one thousand". The grouping of digits is significant to human readers, and some patterns such as "0800" are treated specially. – Michael Kay – 2019-08-14T08:29:33.087

3@PurpleP Anyone interested in clarity of speech, however, especially when speaking over the phone, might want to use "double 6" since it's clearer that the speaker means two sixes and didn't repeat the number 6 accidentally. People aren't robots :P – user91988 – 2019-08-14T20:47:43.020

Answers

10

05AB1E, 53 52 51 50 49 bytes

γε€T2äθ¬MÊi¨₃1ǝR]˜“Šç€µ‚•„í†ìˆÈŒšï¿Ÿ¯¥Š‹¶½¿“#s踻

Try it online!

Explanation:

γ                      # split input in groups of consecutive equal digits
 ε              ]      # for each group
  €T                   #  add a 10 before each digit (66 -> [10, 6, 10, 6])
    2äθ                #  keep only the second half of that list
       ¬MÊi     ]      #  if the first element is not the maximum
           ¨           #   drop the last element
            ₃1ǝ        #   replace the second element with 95
               R       #   reverse the list
˜                      # flatten
 “...“                 # compressed string: "oh one two ... nine double triple"
      #                # split on spaces
       sè              # index (wraps around, so 95 yields "triple")
         ¸»            # join with spaces

Grimmy

Posted 2019-08-13T04:32:58.583

Reputation: 12 521

1Oh, M also looks inside lists when determining the maximum integer on the stack? Didn't knew that. Sounds like something to remember. :) – Kevin Cruijssen – 2019-08-14T06:13:17.443

16

8088 Assembly, IBM PC DOS, 164 159 156 155 bytes

Binary:

00000000: d1ee 8a0c 03f1 53fd ac3a d075 0343 e2f7  ......S..:.u.C..
00000010: 85db 741c 5f8a d043 f6c3 0174 0a57 bd64  ..t._..C...t.W.d
00000020: 0155 83eb 0374 0957 bd5d 0155 4b4b 75f7  .U...t.W.].UKKu.
00000030: 8ad0 2c2f 7213 518a f0b0 24b1 31bf 6a01  ..,/r.Q...$.1.j.
00000040: fcf2 aefe ce75 fa59 57e2 bc5a 85d2 740c  .....u.YW..Z..t.
00000050: b409 cd21 b220 b402 cd21 ebef c364 6f75  ...!. ...!...dou
00000060: 626c 6524 7472 6970 6c65 246f 6824 6f6e  ble$triple$oh$on
00000070: 6524 7477 6f24 7468 7265 6524 666f 7572  e$two$three$four
00000080: 2466 6976 6524 7369 7824 7365 7665 6e24  $five$six$seven$
00000090: 6569 6768 7424 6e69 6e65 24              eight$nine$

Build and test executable using xxd -r from above, or download PHONE.COM.

Unassembled listing:

D1 EE       SHR  SI, 1              ; point SI to DOS PSP (80H) for input string
8A 0C       MOV  CL, BYTE PTR[SI]   ; load input string length into CX
03 F1       ADD  SI, CX             ; move SI to end of input 
53          PUSH BX                 ; push a 0 to signal end of output stack 
        CHAR_LOOP:
FD          STD                     ; set LODS direction to reverse 
AC          LODSB                   ; load next char from [SI] into AL, advance SI 
3A D0       CMP  DL, AL             ; is it same as previous char? 
75 03       JNZ  NEW_CHAR           ; if not, it's a different char 
43          INC  BX                 ; otherwise it's a run, so increment run length
E2 F7       LOOP CHAR_LOOP          ; move on to next char 
        NEW_CHAR: 
85 DB       TEST BX, BX             ; is there a run greater than 0? 
74 1C       JZ   GET_WORD           ; if not, look up digit name 
5F          POP  DI                 ; get name for the current digit 
8A D0       MOV  DL, AL             ; save current char in DL 
43          INC  BX                 ; adjust run count (BX=1 means run of 2, etc)
F6 C3 01    TEST BL, 1              ; is odd? if so, it's a triple
74 0A       JZ   IS_DBL             ; is even, so is a double 
57          PUSH DI                 ; push number string ("one", etc) to stack
BD 0164     MOV  BP, OFFSET T       ; load "triple" string 
55          PUSH BP                 ; push to stack 
83 EB 03    SUB  BX, 3              ; decrement run count by 3 
74 09       JZ   GET_WORD           ; if end of run, move to next input char 
        IS_DBL: 
57          PUSH DI                 ; push number string to stack
BD 015D     MOV  BP, OFFSET D       ; load "double" string 
55          PUSH BP                 ; push to stack 
4B          DEC  BX                 ; decrement by 2
4B          DEC  BX
75 F7       JNZ  IS_DBL             ; if not end of run, loop double again 
        GET_WORD: 
8A D0       MOV  DL, AL             ; save current char into DL
2C 2F       SUB  AL, '0'-1          ; convert ASCII char to 1-based index 
72 13       JB   NOT_FOUND          ; if not a valid char, move to next
51          PUSH CX                 ; save outer loop counter 
8A F0       MOV  DH, AL             ; DH is the index to find, use as scan loop counter
B0 24       MOV  AL, '$'            ; word string is $ delimited
B1 31       MOV  CL, 031H           ; search through length of word data (49 bytes)
BF 016A     MOV  DI, OFFSET W       ; reset word data pointer to beginning
FC          CLD                     ; set DF to scan forward for SCAS 
        SCAN_LOOP: 
F2/ AE      REPNZ SCASB             ; search until delimiter '$' is found in [DI]
FE CE       DEC  DH                 ; delimiter found, decrement counter 
75 FA       JNZ  SCAN_LOOP          ; if counter reached 0, index has been found 
59          POP  CX                 ; restore outer loop position
57          PUSH DI                 ; push string on stack 
        NOT_FOUND:
E2 BC       LOOP CHAR_LOOP          ; move to next char in input 
        OUTPUT_STACK: 
5A          POP  DX                 ; get string from top of stack 
85 D2       TEST DX, DX             ; it is the last? 
74 0C       JZ   EXIT               ; if so, exit 
B4 09       MOV  AH, 09H            ; DOS display string function 
CD 21       INT  21H                ; write string to console 
B2 20       MOV  DL, ' '            ; load space delimiter 
B4 02       MOV  AH, 02H            ; DOS display char function 
CD 21       INT  21H                ; write char to console 
EB EF       JMP  OUTPUT_STACK       ; continue looping 
        EXIT: 
C3          RET                     ; return to DOS 

D   DB "double$" 
T   DB "triple"
W   DB "$oh$","one$","two$","three$","four$","five$","six$","seven$","eight$","nine$" 

TL;DR:

The input string is read right to left to make it easier to find a triple. The output is pushed onto the x86 stack to simplify reversing out the display order and also facilitate rearranging the "double" and "triple" words to precede the digit's name.

If the next digit is different than the last, the name is looked up in the list of words and pushed on to the stack. Since there's no formal concept of an "indexed array of variable-length strings" in machine code, the list of words is scanned i (the word's index) number of times for the string delimiter ($) to find the corresponding word. Helpfully, x86 does have a pair of short instructions (REPNZ SCASB which is similar to memchr() in C), that simplifies this (thanks CISC!).

If the digit is the same as the previous one, the counter for the length of a "run" is incremented and continues looping leftward on the input. Once the run is over, the digit's name is taken from the stack since it will need to placed after the "double" or "triple" for each grouping. If the length of the run is odd (and run length is > 1), the digit's name followed by the string "triple" is pushed to the stack and the run length is reduced by 3. Since the run length will now be even, the step is repeated for "double" until the run length is 0.

When the input string has reached the end, the stack is dumped out with each saved string written to the screen in reverse order.

I/O:

A standalone PC DOS executable, input from command line output to console.

enter image description here

Download and test PHONE.COM.

640KB

Posted 2019-08-13T04:32:58.583

Reputation: 7 149

repne scasb is memchr (or strchr if you know there's going to be a hit), not strstr. – Peter Cordes – 2019-08-15T23:49:39.190

Is CH=0 on process entry guaranteed by a standard, or is that just what some DOS version happens to do? I notice you assume that mov cl, byte[si] is equivalent to movzx cx, byte [si]. I wonder if using a different reg, like AH, for the count with dec ah / jnz instead of loop would save anything from not having to push/pop CX. Probably not, and you don't have any 16-bit regs left that would allow 1-byte dec. – Peter Cordes – 2019-08-15T23:56:22.937

1

@PeterCordes, for the CH=0 I go by http://www.fysnet.net/yourhelp.htm, which for any reasonable release of DOS is always zero'd, same with BX. Good thought about the zero-extend mov, though technically I don't think movzx is available on the 808x (keeping the target platform an IBM PC 5150 and all). I fiddled around with all of the registers as best I could to save the bytes, but if you see something I likely missed, please let me know!

– 640KB – 2019-08-16T00:04:59.337

1It's more accurate to call it memchr IMO. The "string instruction" naming misleads people into thinking they work on implicit-length C strings, but really they work on explicit-length strings like std::string or buffers. Like memcpy, memset (movs/stos), memchr/memrchr (repne scas with DF=0 or 1), and memcmp (repe cmps). The only C equivalent for repe scas is strspn because I don't think there's a mem function for that. You can even describe stosw or stosd as wmemset for example. – Peter Cordes – 2019-08-16T00:06:46.370

1movzx costs an extra opcode byte, and yes it was only introduced with 386. It was just easier to type to describe the fact that you're doing a low-byte-merge and assuming it's correctly zero extended. If you do know CX or at least CH=0, then yes for golfing always go with mov to CL. But outside of golfing, x86's go-to byte-load instructions are movzx and movsx: they avoid any false dependencies or other partial-register shenanigans. On modern CPUs with a dword destination, they're as fast as dword mov loads. – Peter Cordes – 2019-08-16T00:09:50.600

Looking for optimizations, I wonder if we can swap CX and BX so we can use JCXZ instead of test/jz. That's a tradeoff against not being able to use loop for CX, though. So probably not worth it. – Peter Cordes – 2019-08-16T00:23:12.100

If you program for 186 instead of 8086/8, you can use push OFFSET T instead of needing BP as a temporary. push imm8 and push imm16 were new with 186. (https://nasm.us/doc/nasmdocb.html lists when each form of each instruction was introduced.) I don't think being 8086 compatible is particularly special; a .com executable is still a thing that can run on modern CPUs (under a 16 or 32-bit kernel).

– Peter Cordes – 2019-08-16T00:27:36.333

@PeterCordes interesting... using CX instead of BX. I can play around a little bit there and see. I would have to zero CL since it's pretty much guaranteed to be FF. What's bugging me the most is those three byte SUB BX, 3 and TEST BL, 1 but I can't find a way around that doesn't end up costing another byte somewhere else! – 640KB – 2019-08-16T00:57:29.227

You don't have a 1 or odd number in another register, do you? Maybe if one of your static addresses in a register has bit 0 set and bit 1 clear, you could test bx, ?? and use jp to distinguish 0, or 2 from 1, or 3. (You need the 2nd bit cleared for the other AND/TEST input otherwise the parity won't distinguish 0b01 from 0b10. And you know BL <= 3 so you don't care about any higher bits in the static address.) But IDK if that even helps; you have different static addresses floating around in regs at different times. But you could hoist a mov bp, ... out of one branch maybe. – Peter Cordes – 2019-08-16T01:29:06.423

9

05AB1E, 61 56 53 52 51 bytes

γvyDg;LàäRv… ‹¶½¿#yg蓊瀵‚•„í†ìˆÈŒšï¿Ÿ¯¥Š“#yè])áðý

-9 bytes thanks to @Grimy.

Try it online or verify all test cases.

Explanation:

γ               # Split the (implicit) input into substrings of equal adjacent characters
                #  i.e. "199999991779" → ["1","9999999","1","77","9"]
 v              # Loop over each substring `y`:
   Dg           #  Get the length of a copy of the substring
     ;          #  Halve it
      L         #  Create a list in the range [1, length/2], where odd lengths are
                #  automatically truncated/floored
                #   i.e. "1" (length=1) → 0.5 → [1,0]
                #   i.e. "9999999" (length=7) → 3.5 → [1,2,3]
       à        #  Pop and push the maximum of this list
  y     ä       #  Divide the string into that many parts
                #   → ["1"]
                #   → ["999","99","99"]
         R      #  Reverse the list
                #   → ["99","99","999"]
  v             # Inner loop over each item `y`:
   … ‹¶½¿       #  Push dictionary word: "  double triple"
         #      #  Split it on spaces: ["","","double","triple"]
          yg    #  Get the length of the current item `y`
            è   #  And use it to (0-based) index into the list
   “Šç€µ‚•„í†ìˆÈŒšï¿Ÿ¯¥Š“
                #  Push dictionary string "oh two three four five six seven eight nine"
     #          #  Split it on spaces: ["oh","two","three",...,"nine"]
      yè        #  Use `y` to index into the string-list (with automatic wrap-around,
                #  so since there are 10 words, it basically indexes with a single digit
                #  due to an implicit modulo-10)
                #   i.e. "77" → "seven"
 ]              # Close both loops
  )             # Wrap all values on the stack into a list
   á            # Only keep letters, which removes the empty strings from the list
    ðý          # And join the list on spaces
                # (after which the result is output implicitly)

See this 05AB1E tip of mine (section How to use the dictionary?) to understand why … ‹¶½¿ is " double triple" and “Šç€µ‚•„í†ìˆÈŒšï¿Ÿ¯¥Š“ is "oh two three four five six seven eight nine".

Kevin Cruijssen

Posted 2019-08-13T04:32:58.583

Reputation: 67 575

1@Grimy Ah, of course.. I added the if(length>=4) before adding the rest, but of course it isn't necessary for integers of size 1,2,3, because the ;Å2¨3ª£ will leave the strings intact (merely wrapped in a list we flatten after the map anyway). Thanks for noticing! And looking forward seeing your answer with Åγ. I indeed had the feeling the first part could be done a lot shorter somehow. – Kevin Cruijssen – 2019-08-13T11:49:20.010

Here's my current best. Åγ didn't turn out as great as I thought; a better score might be possible by golfing your approach instead. – Grimmy – 2019-08-13T12:25:21.260

@Grimy Not as short as I thought, but yours is still shorter than mine, so obvious +1. I like the single string with added 10/12 indices for the double/triple. Out of curiosity though, why the work-around ¸» when a simple ðý suffices? ;) – Kevin Cruijssen – 2019-08-13T12:32:16.937

It's actually 11 for triple, not 12. Why ðý when ¸» does the job? ðý is a rigid two-byter, while ¸» offers golfing opportunities if you can wrangle the rest of the code such that ¸ is not necessary. – Grimmy – 2019-08-13T12:44:36.540

@Grimy Ah, the 12 was a typo.. And I personally tend to use the most straight-forward approach if byte-counts are the same anyway (unless an equal-bytes but less straight-forward approach is a huge improvement for performance). I personally think using "push space, join by it" is easier to understand for someone reading through the docs than "wrap list in list, join each inner-most list by spaces, and then those strings by newlines (but since there is only a single inner string, no newlines are added)". ;) But feel free to use whatever you'd want; everyone has their own coding style of course. – Kevin Cruijssen – 2019-08-13T12:58:00.827

@Grimy Ah, to y is an obvious one I forgot.. >.> But āɨšJõK is pretty weird I wouldn't have come up with, thanks! Weird how Å¡ transforms it to a list of characters, though.. I personally think it's a bug for strings tbh. – Kevin Cruijssen – 2019-08-13T14:54:21.540

1Dg;LàäR is still a byte shorter than āɨšJõK, and much more similar to what you originally had. – Grimmy – 2019-08-13T15:02:36.273

1@Grimy Ah, that's indeed closed to what I initially had indeed, I like it. :) Thanks again! – Kevin Cruijssen – 2019-08-13T15:27:42.933

1@Grimy Been able to find one more golf myself which I forgot about.. á instead of õK at the end. :) – Kevin Cruijssen – 2019-08-13T15:33:28.113

1

Nice find with á! Here's a 51 and another one. 50 feels possible.

– Grimmy – 2019-08-13T16:06:16.120

@Grimy Ah, nice nested loop. And too bad three contains two adjacent ee, otherwise I could have used ]ðýÔ instead to save that byte. ;) – Kevin Cruijssen – 2019-08-13T16:37:57.030

I did find a few 50s in the end!

– Grimmy – 2019-08-13T18:08:21.543

@Grimy 49 even, nice! – Kevin Cruijssen – 2019-08-14T06:10:41.913

7

QuadR, 137 bytesSBCS

Title case with a leading space.

∊¯2↑¨@(∊∘⎕A)⍵
(.)\1*
{⍺←(,¨⎕D)⎕R('OhOneTwoThreeFourFiveSixSevenEightNine'(∊⊂⊣)⎕A)⋄' '∊w←⍺,⊃⍵:⍬⋄1=≢⍵:⍺⍵⋄3=≢⍵:'Triple',w⋄'Double',w,∇2↓⍵}⍵M

Try it online!

ϵnlist (flatten)
¯2↑¨ take the last two characters (padding on left with a space) of each characters
@ at positions where
(∊∘⎕A) characters are members of the uppercase Alphabet
 in the result of the below PCRE Replace operation…

(.) any character
\1 followed by itself
* zero or more times, is replaced by the result of the following…

{…}⍵M "dfn"; is the Match of the above pattern

('OhOneTwoThreeFourFiveSixSevenEightNine'()⎕A) apply the following anonymous tacit function with the long string and the uppercase Alphabet as left arguments:

 membership (of letters in the long string in the uppercase alphabet)

 partitions (with a new partition beginning whenever is-a-member

 the left argument (i.e. the long string)

()⎕R PCRE Replace the following patterns with those words:

⎕D the digits 0 through 9

 treat each as a separate pattern

⍺← assign this replacement function to (for alphabetise)

then,

⊃⍵ the first character of the match

, as a string

 apply to it

w← assign this to w (for word)

' '∊: if space is a member thereof (i.e. if the match was empty):

 return nothing (becomes the empty string)

 else,

1=≢⍵: if one equals the tally of characters in the match (i.e. its length):

⍺⍵ alphabetise that digit

 else,

3=≢⍵: if three equals the tally of characters in the match (i.e. its length):

'Triple',w prepend "Triple" to the word

 else,

2↓⍵ drop to digits from the match

 recurse on that

w, prepend the word

'Double', prepend "Double"

Adám

Posted 2019-08-13T04:32:58.583

Reputation: 37 779

6

JavaScript (ES6),  161 160 152  144 bytes

The output includes a single leading space.

s=>[n=>' '+'oh/one/two/three/four/five/six/seven/eight/nine'.split`/`[n],'$1 double$2','triple$2'].map(r=>s=s.replace(/(\S*)( \S+)\2|\d/g,r))&&s

Try it online!

or See the formatted source code

How?

The conversion is processed in three steps:

  1. replace each digit with the corresponding English word, preceded by a space
  2. replace each pattern "X X" with "double X"
  3. replace each pattern "double X X" with "triple X"

To save bytes, we use the same regular expression for all steps:

/(\S*)( \S+)\2|\d/g

which works as follows:

(\S*)  -> 1st capturing group: any word, or nothing at all
( \S+) -> 2nd capturing group: a space, followed by a word
\2     -> a copy of the 2nd capturing group
|\d    -> or try to capture a digit instead (for step 1)

At step 1, we use a callback function that picks the correct word from a lookup table:

  • "799999"" seven nine nine nine nine nine"

At step 2, we replace with "$1 double$2":

  • " (seven)( nine)( nine)"" seven double nine"
  • "( nine)( nine) nine"" double nine nine"

At step 3, we replace with "triple$2":

  • " (double)( nine)( nine)"" triple nine"

Arnauld

Posted 2019-08-13T04:32:58.583

Reputation: 111 334

4

Wolfram Language (Mathematica), 115 bytes

{Switch[L=Tr[1^{##}],1," ",3," triple ",_," double "],If[#<1,"oh",IntegerName@#],If[L>3,#0@##3,""]}&@@@Split@#<>""&

Try it online!

Takes a list of digits as input. Output includes a leading space.

attinat

Posted 2019-08-13T04:32:58.583

Reputation: 3 495

4

Stax, 56 bytes

ÇÖ◘⌡¿╒ô╞Γ▓8m☻t7♦3├(Ä⌂≤(┬Ω☻9▲ç╕C╞⌡òσ╗─╣╥─☻╪▼⌡5■ÿ,(┬¥?☺÷•±

Run and debug it

recursive

Posted 2019-08-13T04:32:58.583

Reputation: 8 616

3

Python 2, 171 169 168 bytes

s=input()
while s:c=s[0];n=(s[1:2]==c)+(s[:3]==c*3!=s[1:4]);print'   eellpbiurotd'[-n:0:-2]+'oh one two three four five six seven eight nine'.split()[int(c)],;s=s[1+n:]

Try it online!

-1 byte, thanks to Jitse

TFeld

Posted 2019-08-13T04:32:58.583

Reputation: 19 246

Beat me again! Save 1 byte like so

– Jitse – 2019-08-13T09:14:08.457

@Jitse, that doesn't work for 1312 ;) – TFeld – 2019-08-13T09:43:27.600

Ah, you're right! – Jitse – 2019-08-13T09:45:09.560

How about this one then: ['','double ','triple '][n] to ' eellpbiurotd'[-n:0:-2] for 168 bytes: Try it online!

– Jitse – 2019-08-13T09:52:02.217

Alternatively, also 168 bytes

– Jitse – 2019-08-13T10:13:35.703

@Jitse Thanks :) – TFeld – 2019-08-13T13:13:24.917

3

Scala, 213 bytes

Got it. Somehow the recursive version I was trying to build was strongly more verbose-y than this one (still recursive though, but only in one case). Function f takes as an input string the phone number and outputs its phonetics with a trailing whitespace.

var u="oh one two three four five six seven eight nine" split " "
"(.)\\1*".r.replaceAllIn(s,x=>{var o=x.matched
var k=u(o(0)-48)+" "
o.length match{case 3=>"triple "+k
case 1=>k
case _=>"double "+k+f(o drop 2)}})

Try it online!
Edit : -8b thanks to DrY Wit!

Scala, 215 bytes

And here comes the leading whitespace version, two bytes longer for some reason (even with massive refactoring).

var u="oh one two three four five six seven eight nine" split " "
"(.)\\1*".r.replaceAllIn(s,x=>{var o=x.matched
var k=u(o(0)-48)
" , double , triple ".split(",")(if(o.length>3){k+=f(o drop 2);1}else o.length-1)+k})

Try it online!

V. Courtois

Posted 2019-08-13T04:32:58.583

Reputation: 868

2You can save 8 bytes by replacing (o(0)+"").toInt with o(0)-48. – Dr Y Wit – 2019-08-14T03:40:22.547

Oof well done @DrYWit thanks! – V. Courtois – 2019-08-14T06:28:08.597

3

PHP, 174 169 166 159 bytes

for(;$s=strspn($argn,$d=$argn[$i],$i++);$s==3?($i+=2)+print'triple ':$s<2?:++$i+print'double ',print[oh,one,two,three,four,five,six,seven,eight,nine][$d].' ');

Try it online!

For each digit at index of $i starting from 0:

  • If span of the same digit starting from location of $i is equal to 3, prints 'triple ' and adds 2 to $i so next iteration will have 2 digits jumped over.
  • If span of the same digit starting from location of $i is equal to or more than 2 but is not 3, prints 'double ' and adds 1 to $i so next iteration will have 1 digit jumped over.
  • Prints word for the digit and a space.
  • $i++.

Night2

Posted 2019-08-13T04:32:58.583

Reputation: 5 484

3

Perl 5 -p, 111 bytes

s/(\d)\1/ double$1/g;s/\w+(\d)\1/triple$1/g;s/\d/' '.qw(oh one two three four five six seven eigth nine)[$&]/ge

Try it online!

Explanation:

s/(\d)\1/ double$1/g; # Replace non-overlapping double digits with " double<digit>"
s/\w+(\d)\1/triple$1/g; # Replace remaining double digits preceded by "double" with "triple<digit>"
s/\d/' '.qw(oh one two three four five six seven eigth nine)[$&]/ge # Replace digits with " <word>"

wastl

Posted 2019-08-13T04:32:58.583

Reputation: 3 089

1

Shaved a few bytes off: 106

– Xcali – 2019-08-14T04:43:16.303

2

Perl 6, 96 93 bytes

{S:g/(.)$0?[$0{}<!$0>]?/{(<triple double>X$)[3-$/.comb]}{+$/??uniname(~$0).words[1]!!'oh'} /}

Try it online!

This is an anonymous code block that takes a number and returns a string with the numbers in uppercase, e.g. 0123 => oh ONE TWO THREE with a single trailing space.

This was deleted for a while until I found out how to use captures in a lookahead, but it should be fixed now.

Jo King

Posted 2019-08-13T04:32:58.583

Reputation: 38 234

2

T-SQL 2017, 238 bytes

Added some linebreaks to make it readable

WHILE''<left(@,1)SELECT @=stuff(@,1,iif(p<4,p,2),'')+
iif(p=1,' ',iif(p=3,' triple ',' double '))
+trim(substring('oh  one  two  threefour five six  seveneightnine',left(@,1)*5,5))
FROM(SELECT~-patindex('%[^'+left(@,1)+']%'+'^',@)p)z
PRINT @

Try it online

t-clausen.dk

Posted 2019-08-13T04:32:58.583

Reputation: 2 874

2

Retina 0.8.2, 105 bytes

+`(.)\1
=$1
.
 $&
= =
triple
=
double
9
nine
8
eight
7
seven
6
six
5
five
4
four
3
three
2
two
1
one
0
oh

Try it online! Outputs a leading space. Explanation: I originally tried a regex that automatically matches 2 or 3 digits but @Arnauld's approach of turned out to be golfier. Explanation:

+`(.)\1
=$1

Match pairs of identical digits and replace the first with a =. Then repeat, so that for an odd number the second last digit is also replaced with a =.

.
 $&

Space the digits (and =s) out.

= =
triple

Handle the case of three identical digits.

=
double
9
nine
8
eight
7
seven
6
six
5
five
4
four
3
three
2
two
1
one
0
oh

Replace all remaining characters with words.

Neil

Posted 2019-08-13T04:32:58.583

Reputation: 95 035

2

Jelly, 59 bytes

⁵;`,0;$Ɗ€Ẏ;`Ø0ṭ;-œṣjƭƒV€‘$ị“¡ıc⁴Ṛ⁽]@ɱ2¦Ż©Ẉḷ$Æ!)ɗ[ı8ɱḃ%ċ»Ḳ¤K

Try it online!

A monadic link that takes a string of digit characters as its argument and returns a Jelly string of space-separated words. When called as a full-program, outputs implicitly.

Nick Kennedy

Posted 2019-08-13T04:32:58.583

Reputation: 11 829

2

C++, 382 bytes

It's not superclever, but someone needed to write a C++ version. The recursive function R goes through the input string and counts repeated values. If there are more than 3 repeats, it pretends there were 2 repeats, then rewinds and tries again.

A few more source chars could probably be squeezed out with #define major, but I'm sure a better algo could squeeze out more.

#include <iostream>
#include <sstream>
using namespace std;
char*n[]={"oh","one","two","three","four","five","six","seven","eight","nine"};
void R(ostream& s,const char*r,char p='x',int c=0){if(*r==p)R(s,r+1,p,c+1);else
{if(c>1){if(c>= 4){s<<"double ";r-=(c-2);}else if(c==3)s<< "triple ";else if(c==2)s<< "double ";
}if(c >0)s<<n[p-'0']<<" ";if(!*r)return;R(s,r+1,*r,1);}}

void check(const char* in, const char* out)
{
    std::stringstream ss;
    R(ss,in);
    if (out == ss.str()) std::cout << "PASS: "; else std::cout << "FAIL! ";
    std::cout << in << "\n< " << out << "\n> " << ss.str() << std::endl;
}

int main(int c,char**argv)
{
    if (argv[1] == std::string("test"))
    {
        check("0123"         ,"oh one two three ");
        check("4554554"      ,"four double five four double five four ");
        check("000"          ,"triple oh ");
        check("00000"        ,"double oh triple oh ");
        check("66667888"     ,"double six double six seven triple eight ");
        check("19999999179"  ,"one double nine double nine triple nine one seven nine ");
    }
    else
    {
        char* v = argv[1];
        R(std::cout,v);
        std::cout << std::endl;
    }
}

and verification of test cases:

pa-dev01$ ./a.out test
PASS: 0123
< oh one two three
> oh one two three
PASS: 4554554
< four double five four double five four
> four double five four double five four
PASS: 000
< triple oh
> triple oh
PASS: 00000
< double oh triple oh
> double oh triple oh
PASS: 66667888
< double six double six seven triple eight
> double six double six seven triple eight
PASS: 19999999179
< one double nine double nine triple nine one seven nine
> one double nine double nine triple nine one seven nine

Mark Lakata

Posted 2019-08-13T04:32:58.583

Reputation: 1 631

1Does the golfed part actually need #include <sstream>? Or could you move that down after the golfed part for the test function? I think typing std::ostream&s would take less space than using namespace std;, unless there are other places where you'd need a std::. – Peter Cordes – 2019-08-16T00:35:48.710

253 bytes – ceilingcat – 2019-09-06T22:35:51.187

1

(Roblox) Lua 5.1, 166 bytes

for I,N in('111 triple 11 double 1 '):gmatch'(%d+)(%D+)'do for i,n in('0oh1one2two3three4four5five6six7seven8eight9nine'):gmatch'(.)(%l+)'do s=s:gsub(i*I,N..n)end end

Ensure s is a predefined string value populated only with digits; that will be the variable to be modified. The result will include a leading space [\u20] character.

VisualPlugin Rōblox

Posted 2019-08-13T04:32:58.583

Reputation: 11

Welcome to the site! As Lua can take input via standard methods, it's against the rules to require s to already have the input. Aside from that, you've got a good first post! I would recommend you include a link to an online testing site e.g. https://tio.run/#lua so that others can test your solution

– caird coinheringaahing – 2019-08-13T09:30:14.813

Hi. The variant of Lua I was testing on (Rbx.Lua) doesn't contain input methods, although the sandbox has print, warn, and error output methods. – VisualPlugin Rōblox – 2019-08-14T17:36:34.703

1

Red, 242 bytes

func[s][b:[copy t skip t]parse s[any[change[b t ahead not t](rejoin["triple "t])|
change b(rejoin["double "t])| skip]]foreach c s[prin either i:
find"0123456789"c[rejoin[pick[:oh:one:two:three:four:five:six:seven:eight:nine]index? i" "]][c]]]

Try it online!

Galen Ivanov

Posted 2019-08-13T04:32:58.583

Reputation: 13 815

1

Scala, 253 bytes

def g(s:String):String={val t="oh one two three four five six seven eight nine".split(" ")(s(0)-48)
s.length match{case 3=>"triple "+t;case 2=>"double "+t;case 1=>t;case _=>"double "+t+" "+g(s drop 2)}}
s=>"(.)\\1*".r.findAllIn(s).map(g(_)) mkString " "

Try it online!

Dr Y Wit

Posted 2019-08-13T04:32:58.583

Reputation: 511

1

Oracle SQL, 578 bytes (in formatted form)

Solution is not concise by any means so posting it in formatted way.

with r(s) as
(select x from t
  union all
 select case
           when length(regexp_substr(s, '(.)(\1)+')) = 3 
           then regexp_replace(s, '^...')
           else regexp_replace(s, '^(.)\1|^.')
        end
   from r
  where s is not null)
select listagg(decode(length(r),  2, 'double ',  3, 'triple ') ||
               decode(substr(r, 1, 1), 0, 'oh', to_char(to_date(substr(r, 1, 1), 'j'), 'jsp')), ' ')
               within group (order by rownum)
  from (select regexp_replace(s, lag(s) over (order by length(s)) || '$') r
          from r order by length(s) desc);

Test in SQL*Plus

SQL> create table t(x) as select /*'45547777777774'*/ '1999999910079' from dual;

Table created.

SQL> set pages 0
SQL> with r(s) as
  2  (select x from t
  3    union all
  4   select case
  5             when length(regexp_substr(s, '(.)(\1)+')) = 3
  6             then regexp_replace(s, '^...')
  7             else regexp_replace(s, '^(.)\1|^.')
  8          end
  9     from r
 10    where s is not null)
 11  select listagg(decode(length(r),  2, 'double ',  3, 'triple ') ||
 12                 decode(substr(r, 1, 1), 0, 'oh', to_char(to_date(substr(r, 1, 1), 'j'), 'jsp')), ' ')
 13                 within group (order by rownum)
 14    from (select regexp_replace(s, lag(s) over (order by length(s)) || '$') r
 15            from r order by length(s) desc);
one double nine double nine triple nine one double oh seven nine

The main trick is that digits converted into words using Oracle format models instead of hard-coded literals "one" ... "nine".

Dr Y Wit

Posted 2019-08-13T04:32:58.583

Reputation: 511

any chance of this getting golfed ? it seems you can remove a bunch of spaces. I also imagine you can rewrite WHERE s is not null to WHERE s>'' – t-clausen.dk – 2019-08-14T14:04:44.860

1You can save a few characters by replacing what’s after union all with select regexp_replace(s,case when length(regexp_substr(s, '(.)(\1)+')) = 3 then '^...' else '^(.)\1|^.' end) from r. – Steve Kass – 2019-08-15T16:41:51.487

1

JavaScript, 142 bytes

s=>s.replace(/(.)(\1\1(?!\1)|\1|)/g,t=>(t[2]?' triple ':t[1]?' double ':' ')+'oh one two three four five six seven eight nine'.split` `[t[0]])

Try it online!

tsh

Posted 2019-08-13T04:32:58.583

Reputation: 13 072