17
\$\begingroup\$

Cell Phone Keyboard Typing

This question was asked a while back, but was closed due to poor specs. So, I'm redoing it, with better specs. This question is related, but goes in the opposite direction.

Before T9 came around, in order to type a character in a text message, you had to press one of the number keys a number of times to get the character you wanted. For reference, here is the standard mapping:

+-------+-------+-------+
|   1   |   2   |   3   |
|  .?!1 |  ABC2 |  DEF3 |
+-------+-------+-------+
|   4   |   5   |   6   |
|  GHI4 |  JKL5 |  MNO6 |
+-------+-------+-------+
|   7   |   8   |   9   |
| PQRS7 |  TUV8 | WXYZ9 |
+-------+-------+-------+
|   *   |   0   |   #   |
|   ←   |SPACE 0|   →   |
+-------+-------+-------+

* is backspace, 0 is a space (' ') or the number 0, and # confirms the current character. For the sake of simplicity, all characters are uppercase.

When you pressed a key multiple times, the selected character cycles through the possible characters for that key: 2 -> A, 22 -> B, 222 -> C, 2222 -> 2, 22222 -> A, and so on. Note that, since * only has one option, pressing it repeatedly causes multiple backspaces to be entered. Pressing # multiple times in a row has no effect. A trailing # is unnecessary.

Additionally, if a different key is pressed immediately after pressing a key, the previous keypress is automatically confirmed. Thus, 223 is functionally identical to 22#3.

Your challenge is to translate a series of keypresses into the corresponding string a cell phone would display.

Examples

8#99999#055#33#999#22#666#2#777#3#1 -> T9 KEYBOARD
4433555#55566609666666677755533*3111 -> HELLO WORLD!
7##222#222**7#222#4 -> PPCG
00#0#00 -> 0 0

Rules

  • This is , so the shortest correct solution (in bytes) wins
  • The winning answer will be chosen in one week
  • Standard loopholes are forbidden
  • Your answer may be in the form of a full program, a named function, or an anonymous function, taking input and producing output in one of the standard methods

Leaderboard

The Stack Snippet at the bottom of this post generates the leaderboard from the answers a) as a list of shortest solution per language and b) as an overall leaderboard.

To make sure that your answer shows up, please start your answer with a headline, using the following Markdown template:

## Language Name, N bytes

where N is the size of your submission. If you improve your score, you can keep old scores in the headline, by striking them through. For instance:

## Ruby, <s>104</s> <s>101</s> 96 bytes

If there you want to include multiple numbers in your header (e.g. because your score is the sum of two files or you want to list interpreter flag penalties separately), make sure that the actual score is the last number in the header:

## Perl, 43 + 2 (-p flag) = 45 bytes

You can also make the language name a link which will then show up in the snippet:

## [><>](http://esolangs.org/wiki/Fish), 121 bytes

<style>body { text-align: left !important} #answer-list { padding: 10px; width: 290px; float: left; } #language-list { padding: 10px; width: 290px; float: left; } table thead { font-weight: bold; } table td { padding: 5px; }</style><script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/codegolf/all.css?v=83c949450c8b"> <div id="language-list"> <h2>Shortest Solution by Language</h2> <table class="language-list"> <thead> <tr><td>Language</td><td>User</td><td>Score</td></tr> </thead> <tbody id="languages"> </tbody> </table> </div> <div id="answer-list"> <h2>Leaderboard</h2> <table class="answer-list"> <thead> <tr><td></td><td>Author</td><td>Language</td><td>Size</td></tr> </thead> <tbody id="answers"> </tbody> </table> </div> <table style="display: none"> <tbody id="answer-template"> <tr><td>{{PLACE}}</td><td>{{NAME}}</td><td>{{LANGUAGE}}</td><td>{{SIZE}}</td><td><a href="{{LINK}}">Link</a></td></tr> </tbody> </table> <table style="display: none"> <tbody id="language-template"> <tr><td>{{LANGUAGE}}</td><td>{{NAME}}</td><td>{{SIZE}}</td><td><a href="{{LINK}}">Link</a></td></tr> </tbody> </table><script>var QUESTION_ID = 61545; var ANSWER_FILTER = "!t)IWYnsLAZle2tQ3KqrVveCRJfxcRLe"; var COMMENT_FILTER = "!)Q2B_A2kjfAiU78X(md6BoYk"; var OVERRIDE_USER = 45941; var answers = [], answers_hash, answer_ids, answer_page = 1, more_answers = true, comment_page; function answersUrl(index) { return "https://api.stackexchange.com/2.2/questions/" + QUESTION_ID + "/answers?page=" + index + "&pagesize=100&order=desc&sort=creation&site=codegolf&filter=" + ANSWER_FILTER; } function commentUrl(index, answers) { return "https://api.stackexchange.com/2.2/answers/" + answers.join(';') + "/comments?page=" + index + "&pagesize=100&order=desc&sort=creation&site=codegolf&filter=" + COMMENT_FILTER; } function getAnswers() { jQuery.ajax({ url: answersUrl(answer_page++), method: "get", dataType: "jsonp", crossDomain: true, success: function (data) { answers.push.apply(answers, data.items); answers_hash = []; answer_ids = []; data.items.forEach(function(a) { a.comments = []; var id = +a.share_link.match(/\d+/); answer_ids.push(id); answers_hash[id] = a; }); if (!data.has_more) more_answers = false; comment_page = 1; getComments(); } }); } function getComments() { jQuery.ajax({ url: commentUrl(comment_page++, answer_ids), method: "get", dataType: "jsonp", crossDomain: true, success: function (data) { data.items.forEach(function(c) { if (c.owner.user_id === OVERRIDE_USER) answers_hash[c.post_id].comments.push(c); }); if (data.has_more) getComments(); else if (more_answers) getAnswers(); else process(); } }); } getAnswers(); var SCORE_REG = /<h\d>\s*([^\n,<]*(?:<(?:[^\n>]*>[^\n<]*<\/[^\n>]*>)[^\n,<]*)*),.*?(\d+)(?=[^\n\d<>]*(?:<(?:s>[^\n<>]*<\/s>|[^\n<>]+>)[^\n\d<>]*)*<\/h\d>)/; var OVERRIDE_REG = /^Override\s*header:\s*/i; function getAuthorName(a) { return a.owner.display_name; } function process() { var valid = []; answers.forEach(function(a) { var body = a.body; a.comments.forEach(function(c) { if(OVERRIDE_REG.test(c.body)) body = '<h1>' + c.body.replace(OVERRIDE_REG, '') + '</h1>'; }); var match = body.match(SCORE_REG); if (match) valid.push({ user: getAuthorName(a), size: +match[2], language: match[1], link: a.share_link, }); else console.log(body); }); valid.sort(function (a, b) { var aB = a.size, bB = b.size; return aB - bB }); var languages = {}; var place = 1; var lastSize = null; var lastPlace = 1; valid.forEach(function (a) { if (a.size != lastSize) lastPlace = place; lastSize = a.size; ++place; var answer = jQuery("#answer-template").html(); answer = answer.replace("{{PLACE}}", lastPlace + ".") .replace("{{NAME}}", a.user) .replace("{{LANGUAGE}}", a.language) .replace("{{SIZE}}", a.size) .replace("{{LINK}}", a.link); answer = jQuery(answer); jQuery("#answers").append(answer); var lang = a.language; lang = jQuery('<a>'+lang+'</a>').text(); languages[lang] = languages[lang] || {lang: a.language, lang_raw: lang.toLowerCase(), user: a.user, size: a.size, link: a.link}; }); var langs = []; for (var lang in languages) if (languages.hasOwnProperty(lang)) langs.push(languages[lang]); langs.sort(function (a, b) { if (a.lang_raw > b.lang_raw) return 1; if (a.lang_raw < b.lang_raw) return -1; return 0; }); for (var i = 0; i < langs.length; ++i) { var language = jQuery("#language-template").html(); var lang = langs[i]; language = language.replace("{{LANGUAGE}}", lang.lang) .replace("{{NAME}}", lang.user) .replace("{{SIZE}}", lang.size) .replace("{{LINK}}", lang.link); language = jQuery(language); jQuery("#languages").append(language); } }</script>

\$\endgroup\$
  • 1
    \$\begingroup\$ How do you produce numbers? You have the number '9' in an example, but your specification (2 -> A, 22 -> B..., 2222 -> A....) does not allow any numbers to be produced. \$\endgroup\$ – C. Quilley Oct 23 '15 at 13:48
  • 1
    \$\begingroup\$ @C.Quilley That's what I get for blindly copying that diagram over, fixed now. \$\endgroup\$ – Mego Oct 23 '15 at 13:54
  • \$\begingroup\$ Closely related, possible dupe. \$\endgroup\$ – Alex A. Oct 23 '15 at 20:51
  • 1
    \$\begingroup\$ @AlexA. Not a dupe, it's standard phone numpad input, not T9 dictionary lookup. \$\endgroup\$ – Mego Oct 23 '15 at 20:55
3
\$\begingroup\$

K5, 112 bytes

{(20#'((" ";".?!"),((3*!6),19 22)_`c$65+!26),'$!10)[.*x]20!#1_x}'(){$[42=*y;(-#y)_x;35=*y;x;x,,y]}/{(&~0=':x)_x}

This one is really a mess, but I think there's a fair amount of room to golf it down.

First we need to construct a lookup table for the keymap. There are keys with 2, 4 and 5 characters mapped to them, so padding every entry out to 20 simplifies the process of cyclically indexing this table later:

  (20#'((" ";".?!"),((3*!6),19 22)_`c$65+!26),'$!10)
(" 0 0 0 0 0 0 0 0 0 0"
 ".?!1.?!1.?!1.?!1.?!1"
 "ABC2ABC2ABC2ABC2ABC2"
 "DEF3DEF3DEF3DEF3DEF3"
 "GHI4GHI4GHI4GHI4GHI4"
 "JKL5JKL5JKL5JKL5JKL5"
 "MNO6MNO6MNO6MNO6MNO6"
 "PQRS7PQRS7PQRS7PQRS7"
 "TUV8TUV8TUV8TUV8TUV8"
 "WXYZ9WXYZ9WXYZ9WXYZ9")

Then I split the input into runs:

 {(&~0=':x)_x}"8#99999#055#33#999"
(,"8"
 ,"#"
 "99999"
 ,"#"
 ,"0"
 "55"
 ,"#"
 "33"
 ,"#"
 "999")

Drop any # runs and strip trailing runs every time I encounter a *:

  (){$[42=*y;(-#y)_x;35=*y;x;x,,y]}/{(&~0=':x)_x}"8#99999#055#33#999"
(,"8"
 "99999"
 ,"0"
 "55"
 "33"
 "999")

And then I'm ready to simply index into that lookup table based on the length and first element of each run.

All together:

  {(20#'((" ";".?!"),((3*!6),19 22)_`c$65+!26),'$!10)[.*x]20!#1_x}'(){$[42=*y;(-#y)_x;35=*y;x;x,,y]}/{(&~0=':x)_x}"4433555#55566609666666677755533*3111"
"HELLO WORLD!"

Edit:

Save 5 bytes:

0 3 6 9 12 15 19 22
((3*!6),19 22)
\$\endgroup\$
  • \$\begingroup\$ You can shorten (20#'((" ";".?!"),0 3 6 9 12 15 19 22_`c$65+!26),'$!10) to (20#'((" ";".?!"),((3*!6),19 22)_`c$65+!26),'$!10). \$\endgroup\$ – kirbyfan64sos Oct 23 '15 at 18:43
  • \$\begingroup\$ Yeah, I actually just found that a few minutes ago. \$\endgroup\$ – JohnE Oct 23 '15 at 18:45
3
\$\begingroup\$

Python 2, 230 206 bytes

import re
f=lambda a,b=dict(zip("0123456789*#"," 0~.?!1~ABC2~DEF3~GHI4~JKL5~MNO6~PQRS7~TUV8~WXYZ9~\b~".split("~"))):"".join([j and b[j][(len(i)-1)%len(b[j])]or b[i]for i,j in re.findall(r"((\d)\2*|.)",a)])

This one creates a function f which takes a string of keypresses as argument and returns the corresponding string a cell phone would display. It also happens to take an optional second argument as a dictionary mapping keys to their corresponding characters, eg {"0": " 0", "1": ".?!1", ...}.

First, the string of keypresses is grouped by repeating characters, eg ["8", "#", "99999", "#", ...]. Then, the first character of each group is mapped in the dictionary passed as second argument, eg 9 maps to WXYZ9. Finally, the length of the group is used as offset in the value from the dictionary.

Note that the offset must use modulo on the length of the group of repeating characters because keypresses might cycle. Also note that the # character is mapped to \0 and only removed at the end because 99#99 is not the same as 9999.

Here is the output of the function for each of the examples in the question:

>>> print f("8#99999#055#33#999#22#666#2#777#3#1")
T9 KEYBOARD.
>>> print f("4433555#55566609666666677755533*3111")
HELLO WORLD!
>>> print f("7##222#222**7#222#4")
PPCG
>>> print f("00#0#00")
0 0
\$\endgroup\$
3
\$\begingroup\$

JavaScript, 214 184 168 162 bytes

x=>(x.match(/(.)\1*/g,f='').map(a=>f=(l=a.length,v=' 0#.?!1#ABC2#DEF3#GHI4#JKL5#MNO6#PQRS7#TUV8#WXYZ9'.split`#`[a[0]])?f+v[--l%v.length]:a<'*'?f:f.slice(0,-l)),f)

This can probably be made a little smaller, but I'm pretty happy with the result. Splits characters into repeated groups of one or more, then steps through the array, mapping each character to it's value in the hash and adding it onto the final string. If it comes across any number of '#', it ignores it. If it comes across any '*', it removes that number of them from the end of the final string.

\$\endgroup\$
0
\$\begingroup\$

Python 2, 237 bytes

Using cr3's dictionary, but without re.

def f(i):
 d=dict(zip("0123456789"," 0|.?!1|ABC2|DEF3|GHI4|JKL5|MNO6|PQRS7|TUV8|WXYZ9".split("|")))
 s,x,j='',i[0],0
 for c in i[1:]+'#':
  if c==x:j+=1
  else:
   if x>'/':s+=d[x][j%len(d[x])]
   j=0
  if c=='*':s=s[:-1]
  x=c
 return s
\$\endgroup\$
-1
\$\begingroup\$

Python 2, 265 bytes

It's way too long. IO: stdin, stdout.

a=reduce(lambda q,w:q+" "+[w,""][w=="#"]if q[-1]!=w else q+w,raw_input()).split()
while "*" in a:del a[a.index("*")-1:a.index("*")+1]
print"".join([(lambda a:a[len(q)%len(a)-1])(" 0:.?!1:ABC2:DEF3:GHI4:JKL5:MNO6:PQRS7:TUV8:WXYZ9".split(":")[int(q[0])])for q in a])
\$\endgroup\$
  • \$\begingroup\$ The third example, 7##222#222**7#222#4, will cause your script to raise a ValueError: invalid literal for int() with base 10: '*'. \$\endgroup\$ – cr3 Oct 24 '15 at 21:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.