While I was writing numbers I noticed after a while that my keyboard had the Shift key pressed and blocked and all I wrote was $%&-like characters. And even worse, I had been switching between the English and Spanish keyboard layouts so I don't know which one I used for each number.


Given a string containing symbol characters, try to guess which number I wrote. My keyboard produces the following characters for the numbers when the Shift is pressed:

!"·$%&/()=  Spanish layout
!@#$%^&*()  English layout
  • The input will be a non-null, non-empty string composed of the symbols above.
  • The output will be a single number if the keyboard layout can be inferred from the string (i.e. if the string contains a @ an English layout was used, and if the string contains a " a Spanish layout was used) or if the number is the same for both layouts (i.e. the input is !$ which translates as 14 for both layouts); otherwise the output will be the two possible numbers for both layouts if it cannot be inferred and the resulting numbers are different.
  • The input string will always be written in a single layout. So you don't need to expect "@ as input.


Input  -->  Output
/()         789        (Spanish layout detected by the use of /)
$%&         456,457    (Layout cannot be inferred)
!@#         123        (English layout detected by the use of @ and #)
()&!        8961,9071  (Layout cannot be inferred)
((·))       88399      (Spanish layout detected by the use of ·)
!$          14         (Layout cannot be inferred but the result is the same for both)
!!$$%%      114455     (Layout cannot be inferred but the result is the same for both)
==$"        0042/42    (Spanish layout, if a number starts with 0 you can choose to
                       omit them in the result or not)

Single character translations:
!   1    
"   2
·   3
$   4
%   5
&   6,7
/   7
(   8,9
)   9,0
=   0
@   2
#   3
^   6
*   8

This is , so may the shortest code for each language win!

  • \$\begingroup\$ Dang it, that · is challenging... \$\endgroup\$ Oct 12, 2018 at 20:39
  • 2
    \$\begingroup\$ @EriktheOutgolfer in fact the · is useless for Spanish, it is only used in the Catalan language. \$\endgroup\$
    – Charlie
    Oct 12, 2018 at 20:41
  • \$\begingroup\$ Is output like {(8, 9, 6, 1), (9, 0, 7, 1)} (for the 4th test case) acceptable? \$\endgroup\$
    – lynn
    Oct 12, 2018 at 21:01
  • \$\begingroup\$ @Lynn yes, it is. \$\endgroup\$
    – Charlie
    Oct 12, 2018 at 21:03
  • \$\begingroup\$ When outputting 2 numbers, does the order matter? \$\endgroup\$
    – Shaggy
    Oct 12, 2018 at 22:30

14 Answers 14


Jelly, 32 31 bytes


Try it online!

  • -1 bytes thanks to Erik the Outgolfer

O                                  ord of each character in the input
 “!"Ṣ$%&/()=“!@#$%^&*()‘           Constant that yields the list:
                                      [[33, 34, 183, 36, 37, 38, 47, 40, 41, 61],
                                       [33, 64, 35, 36, 37, 94, 38, 42, 40, 41]
                          €        For each list of numbers:
                         Ɱ           For each ord of the characters in the input:
                        i              Find the index of the ord of the character
                                       in the list of numbers.
                                       If the number is not found, `i` returns zero
                                       which means it's a character from only one
                                   There are now two lists of numbers 1-10.
                            Ƈ      Keep the list(s) that: 
                           P         have nonzero product.
                             %⁵    Modulo 10. This maps 10->0.
                               Q   Unique elements. This removes duplicates if the two numbers are the same.

Python 3, 76 bytes

lambda s:{(*map(k.find,s),)for k in['=!"·$%&/()',')!@#$%^&*(']if{*s}<={*k}}

Try it online!


Perl 6, 62 bytes

{set grep {!/\D/},TR'=!"·$%&/()'0..9',TR')!@\x23$%^&*('0..9'}

Try it online!

Returns a Set. (Could be made two or three bytes shorter if there wasn't a bug in Rakudo's handling of # in search lists.)


Java (JDK), 173 bytes


c->{String s="",e=s;var m="21#3457#908###6##12#456389###0#7".split("");for(int l:c){e+=m[l=l%16];s+=m[l+16];}return s.equals(e)|s.contains("#")?e:e.contains("#")?s:s+","+e;}

Try it online!


c->{                                                      // Lamdba taking char array as input
    String s="",e=s;                                      // Initialise Spanish and English strings
    var m="21#3457#908###6##12#456389###0#7".split("");   // Create magic hashing lookup array (see below)
    for(int l:c){                                         // Loops through all chars in input
        e+=m[l=l%16];                                     // Get english number from array and append
        s+=m[l+16];                                       // Get Spanish number from array and append
    return s.equals(e)|s.contains("#")?e:                 // If equal or Spanish is invalid return english
        e.contains("#")?s:                                // If English is invalid return Spanish
        s+","+e;                                          // If both are valid but not equal, return both

The Magic Hashing Lookup Array

After some experimenting with values I realised that each of the ASCII values of the characters !"·$%&/()=@#^* modulo 16 returns a unique number. The 'magic hashing lookup array' stores the English numbers associated with each character at this unique index, and each of the Spanish numbers at this index offset by 16, making fetching the required number from the array trivial for each language. A hash is stored for values that are invalid for either language.

  • \$\begingroup\$ I don't suppose you could use toCharArray() and the int values to make this shorter? (Just an idea, I haven't tried it yet.) \$\endgroup\$
    – Quintec
    Oct 13, 2018 at 0:46
  • \$\begingroup\$ @Quintec I tried it, but the extra bytes from toCharArray() and calculating the exponent to be applied to the int value made it far longer than both the .contains() statements. \$\endgroup\$ Oct 13, 2018 at 16:35
  • \$\begingroup\$ s.equals(e)|s.contains("#") can be s.matches(e+"|.*#.*"). \$\endgroup\$ Oct 15, 2018 at 6:41
  • \$\begingroup\$ Suggest l%=16 instead of l=l%16 \$\endgroup\$
    – ceilingcat
    Jan 3, 2020 at 1:26

Japt, 38 bytes

Outputs an array of strings with the Spanish layout first.

"=!\"·$%&/())!@#$%^&*("òA £ËXbD
kø'- â

Try it


Jelly, 38 bytes


Try it online!

  • \$\begingroup\$ Nice! Just one question, I have tried your code with () or (()) as input, but your code then return nothing. I suppose that's a limitation with what Jelly receives as input? \$\endgroup\$
    – Charlie
    Oct 12, 2018 at 21:27
  • 1
    \$\begingroup\$ @Charlie Try with '()' and '(())' respectively. Yes, if you don't quote the argument, it's only inputted as a string if it can't be evaluated to a Python 3 value. \$\endgroup\$ Oct 12, 2018 at 21:28

Retina 0.8.2, 60 bytes


Try it online! Link includes test cases. Explanation:


Duplicate the input.


Try to translate each line according to a different keyboard layout.


Deduplicate the result.


Only keep lines that only contain digits.

  • \$\begingroup\$ Do you need the m in your last stage? \$\endgroup\$
    – ovs
    Oct 13, 2018 at 10:38
  • \$\begingroup\$ @ovs Yes, the matches run first, and then the lines are split and lines containing matches are kept. \$\endgroup\$
    – Neil
    Oct 13, 2018 at 12:12

JavaScript (ES6), 99 bytes


Try it online!


The helper function \$g\$ attempts to convert the input string using a given layout.

Invalid characters are replaced with \$-1\$, which results in either a valid but negative looking numeric string (if only the first character was missing), or an invalid numeric string. Either way, the test x >= 0 is falsy.


05AB1E, 42 41 bytes


Port of @dylnan's Jelly answer.

Try it online or verify all test cases.


-•184в           # Compressed list [33,34,183,36,37,38,47,40,41,61,33,64,35,36,37,94,38,42,40,41]
      2ä         # Split into two parts: [[33,34,183,36,37,38,47,40,41,61],[33,64,35,36,37,94,38,42,40,41]]
ε   }            # Map each inner list to:
 IÇ              #  Get the input, and convert each character to its unicode value
   k             #  Then get the index of each unicode value in the current map-list
                 #  (this results in -1 if the item doesn't exist)
     ʒ   }       # Filter the resulting list of indices by:
      ®å_        #  If the inner list does not contain any -1
          >      # Increase each index by 1 to make it from 0-indexed to 1-indexed
           T%    # Take modulo-10 to convert 10 to 0
             Ù   # Uniquify the result-lists (and output implicitly)

See this 05AB1E tip of mine (section How to compress integer lists?) to understand why •Hhç₁d©u÷^Σ(“ðΣèõĆ\n-•184в is [33,34,183,36,37,38,47,40,41,61,33,64,35,36,37,94,38,42,40,41]). This (together with the ) is 1 byte shorter than taking the unicode values of the string: '""!ÿ·$%&/()=""!@#$%^&*()"‚Ç.

  • \$\begingroup\$ The !$ and !!$$%% cases should output only one number as the result is the same for both layouts and there is no ambiguity. \$\endgroup\$
    – Charlie
    Oct 15, 2018 at 7:17
  • \$\begingroup\$ @Charlie Oops, fixed \$\endgroup\$ Oct 15, 2018 at 7:21

Ruby, 68 bytes

->s{%w[=!"·$%&/() )!@#$%^&*(].map{|a|s.tr a,'0-9'}.grep_v(/\D/)|[]}

Try it online!


Clean, 116 bytes

import StdEnv,Text
$s=removeDup[foldl(<+)""d\\r<-["=!\"·$%&/()",")!@#$%^&*("],d<-[[indexOf{c}r\\c<-s]]|all((<) -1)d]

Try it online!

Takes input and is encoded in CP437. TIO only supports UTF-8, so an escape is used in the demo code to get the literal byte value 250 corresponding to the centre dot (counted as one byte).

  • \$\begingroup\$ The !$% input should output only one number, not two, as the result is the same for both layouts. \$\endgroup\$
    – Charlie
    Oct 15, 2018 at 7:15
  • \$\begingroup\$ @Charlie Fixed it. \$\endgroup\$
    – Οurous
    Oct 15, 2018 at 8:30

APL (Dyalog), 40 bytes

Anonymous tacit prefix function. Though unused, · is in the Dyalog single byte character set. Assumes 0-based indexing (⎕IO←0) which is default on many systems.

{∪⍵/⍨~10∊¨⍵}'=!"·$%&/()' ')!@#$%^&*('⍳¨⊂

Try it online!

 the entire argument

'=!"·$%&/()' ')!@#$%^&*('⍳¨ indices of the characters in each of these strings

{∪⍵/⍨~10∊¨⍵} apply the following lambda ( is the argument):

10∊¨⍵ for each list of digits, whether 10 (indicating "not found") is a member thereof

~ local negation (i.e. only those where all digits are found)

⍵/⍨ filter the argument by that

 find the unique elements of that


Dart, 125 bytes


Ungolfed :

  • Creates an array with the two specified key values, from 0 to 9
  • For each of them, convert the input string to the corresponding number by using the characters' indexes
  • Join the resulting array to create a number
  • Remove any number having a '-' (Dart returns -1 when indexOf can't find a char)
  • Return as a Set to remove duplicates

Try it on Dartpad!


T-SQL, 143 bytes

FROM i,(VALUES('!"·$%&/()='),('!@#$%^&*()'))t(m))b

Input is taken via pre-existing table i with varchar field v, per our IO standards.

Joins the input table with the two different character strings, then uses the new SQL 2017 function TRANSLATE to swap out individual characters, and TRY_CAST to see if we end up with a number. If not, TRY_CAST returns NULL.

The final outer SELECT DISTINCT combines identical results and filters out the NULLS.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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