Spanish ID card control character calculator

20

3

This is a very very simple algorithm, that I am sure can be solved in many many different languages. In Spain ID cards (known as DNI) consist of 8 numbers and a control character. The control character is calculated with the following algorithm: divide the number by 23, take the remainder of the operation and replace it with a character according to this table:

0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22  
T  R  W  A  G  M  Y  F  P  D  X  B  N  J  Z  S  Q  V  H  L  C  K  E

If the DNI belongs to a foreign person living in Spain, the first digit is changed to X, Y or Z and it is called an NIE. In this case, the following substitutions are made before calculating the control character:

X Y Z
0 1 2

There are a lot of calculators online that help you get the control character, but, how short can you write that code? Write an algorithm (program or function) that receives a string with the DNI number (that will always consist of 8 alphanumeric characters) and returns just the single control character calculated and nothing more (a trailing newline is accepted).

Notes:

  • The DNI is always written in uppercase, but in your algorithm you can choose the input and output to be upper- or lowercase, just be consistent.
  • In real life, some NIEs issued before 2008 have 8 digits after the X, Y or Z, but for the purposes of this game, you can consider they have 7 digits as they have nowadays.
  • You can consider that the input string will always have 8 characters, but if they are not in the "8 digits" format nor the "[XYZ] plus 7 digits" format, you must return an error (of your choice) or just throw an exception.

Test cases:

00000010 -> X (HRM Juan Carlos I's DNI number)
01234567 -> L
98765432 -> M
69696969 -> T
42424242 -> Y
Z5555555 -> W (Z=2)
Y0000369 -> S (Y=1)
A1234567 -> <Error code or exception>
1231XX12 -> <Error code or exception>

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

Charlie

Posted 2017-07-03T06:18:57.953

Reputation: 11 448

Sandbox. – Charlie – 2017-07-03T06:19:24.047

2Is it really important that the code have a specific behavior on invalid input? Usually challenges here don't require worrying about error handling. – Greg Martin – 2017-07-03T08:26:28.680

3@GregMartin my point precisely, I just wanted the code to show some specific behaviour on error inputs as it is not usually required. – Charlie – 2017-07-03T08:32:18.107

In “divide the number by 23, take the rest of the operation”, the correct term is remainder; rest is too colloquial.

– Locoluis – 2017-07-03T19:14:59.933

2@Locoluis in Spanish we say resto, making "rest" a false friend, then. At least I didn't use a wrong term. :-) Thank you! – Charlie – 2017-07-03T19:19:20.067

My NIE is pre-2008 (by a large stretch) and it's 7 digits... ;-) – Rmano – 2017-07-04T11:10:54.813

Incredible, I came to post the exact same thing but for the french equivalent (97 - Number // 97) and you posted that just yesterday. – Jylo – 2017-07-04T11:31:56.410

Answers

11

Python 3, 83 bytes

lambda n:'TRWAGMYFPDXBNJZSQVHLCKE'[int([n,str(ord(n[0])%4)+n[1:]][n[0]in'XYZ'])%23]

Try it online!

-5 thanks to AlixEinsenhardt (from 99 to 94). -1 thanks to JonathanAllan.

Mr. Xcoder

Posted 2017-07-03T06:18:57.953

Reputation: 39 774

1You can replace str('XYZ'.index(n[0])) by str(ord(n[0])-88) and save 5 bytes – Alix Eisenhardt – 2017-07-03T09:28:09.627

1@AlixEisenhardt The above suggestion inspired me to change the technique to a lambda, which eventually saved 10 bytes. – Mr. Xcoder – 2017-07-03T09:42:26.123

Save a byte by replacing -88 with %4. – Jonathan Allan – 2017-07-03T23:29:59.890

8

Haskell, 107 93 92 bytes

c(x:y)="TRWAGMYFPDXBNJZSQVHLCKE"!!mod(read(("X0Y1Z2"!x):y))23
(a:b:c)!x|x==a=b|2>1=c!x
_!x=x

Try it online!

bartavelle

Posted 2017-07-03T06:18:57.953

Reputation: 1 261

What's the behaviour on invalid inputs? – Charlie – 2017-07-03T07:08:30.113

They will crash the program, I added one in the example. (in practice it throws an exception that nobody catches) – bartavelle – 2017-07-03T07:12:14.363

1I updated the submission with exception catching, so that all tests could be run. – bartavelle – 2017-07-03T07:17:26.773

5

Pyth, 35 34 bytes

The code contains some unprintable characters, so here is a reversible xxd hexdump.

00000000: 402e 5043 22fc eeff 1ffc adc7 e614 9451  @.PC"..........Q
00000010: 2247 2573 7358 637a 5d31 3e33 4755 3320  "G%ssXcz]1>3GU3
00000020: 3233                                     23

Uses lowercase characters.

Try it online. Test suite.

Printable version

@.P305777935990456506899534929G%ssXcz]1>3GU3 23

Explanation

  • cz]1 splits the input at position 1, e.g. "y0000369" to ["y", "0000369"].
  • >3G gets the last 3 characters of the alphabet, "xyz".
  • U3 gets the range [0, 3[, [0, 1, 2].
  • X maps xyz to [0, 1, 2] in the split array, e.g. ["y", "0000369"] to [1, "0000369"]. This replaces the first character if it is one of xyz, while leaving the tail of 7 characters untouched since any 7 character string can't be equal to a single character.
  • s joins the array with the empty string, e.g. [1, "0000369"] to "10000369".
  • s casts this string to integer, e.g. "10000369" to 10000369. This throws an error if any extra non-digit characters are left in the string.
  • %23 gets the value modulo 23, e.g. 10000369 to 15.
  • C"" converts the binary string from base 256 to integer (about 3.06 × 1026).
  • .PG gets the permutation of the alphabet with that index.
  • @ gets the correct character from the permutation.

PurkkaKoodari

Posted 2017-07-03T06:18:57.953

Reputation: 16 699

4

MATL, 62 59 bytes

'RWAGMYFPDXBNJZSQVHLCKET'j'[\dXYZ]\d{7}'XXg'XYZ'I:47+XEU1))

The error for not valid input is A(I): index out of bounds (compiler running in Octave) or Index exceeds matrix dimensions (compiler running in Matlab).

Try it online!

Explanation

'RWAGMYFPDXBNJZSQVHLCKET' % Push this string (output letters circularly shifted by 1)
j                         % Unevaluated input
'[\dXYZ]\d{7}'            % Push this string (regexp pattern)
XX                        % Regexp. Returns cell arary with matching string, or empty
g                         % Convert to standard array. Will be empty if non-valid input
'XYZ'                     % Push this string
I:47+                     % Push [47 48 49] (ASCII codes of '012')
XE                        % Transliterate
U                         % Convert to number
1)                        % Get first entry. Gives an error if empty
)                         % Index (modular, 1-based) into initial string
                          % Implicitly display

Luis Mendo

Posted 2017-07-03T06:18:57.953

Reputation: 87 464

4

ES6, 83 82 81 bytes

i=>'TRWAGMYFPDXBNJZSQVHLCKE'[(/^[XYZ]/.test(i)?i.charCodeAt()%4+i.slice(1):i)%23]

In action!

Uppercase only, the error code for invalid numbers is undefined.

One byte saved thanks to Jonathan Allan.
Another byte saved thanks to Shaggy.

2ndAttmt

Posted 2017-07-03T06:18:57.953

Reputation: 131

Maybe save a byte using %4 rather than -88. – Jonathan Allan – 2017-07-03T23:32:39.073

You should be able to drop the 0 from charCodeAt() too. – Shaggy – 2017-07-04T09:26:01.747

3

Java 8, 154 145 104 bytes

s->{s[0]-=s[0]<88|s[0]>90?0:40;return"TRWAGMYFPDXBNJZSQVHLCKE".charA‌​t(new Integer(new String(s))%23);}

-9 bytes thanks to @OliverGrégoire.
-41 bytes thanks to @OliverGrégoire again, by taking the input as a char-array (char[]).

If the input is invalid, it will either fail with a java.lang.NumberFormatException or java.lang.StringIndexOutOfBoundsException.

Explanation:

Try it here. (Invalid test cases are surrounded by try-catch so it doesn't stop at the first error.)

s->{                      // Method with char[] parameter and char return-type
  s[0]-=s[0]<88|s[0]>90?  // If the first character is not XYZ:
    0                     //  Leave the first character as is
   :                      // Else:
    40;                   //  Subtract 40 to convert it to 012
  return"TRWAGMYFPDXBNJZSQVHLCKE".charAt(
                          //    Get the char from the String
    new Integer(          //    by converting the following String to an integer:
      new String(s)       //     by converting the char-array to a String
    )%23);                //    And take modulo-23 of that integer
}                         // End of method

Kevin Cruijssen

Posted 2017-07-03T06:18:57.953

Reputation: 67 575

1You don't need the | in the regex. Also int t=s.charAt(0)-88 & t<0?t+40:t spare you a byte. – Olivier Grégoire – 2017-07-04T14:09:32.453

1

Finally, you can return an error code. Just decide that it's 'a' or '0' or any non uppercase letter, and return that instead of t/0 and casting the whole lot to char. You'd save 7 bytes this way, I guess. Golfed this way, you get 145 bytes.

– Olivier Grégoire – 2017-07-04T14:20:22.117

1@OlivierGrégoire Thanks! I have the feeling it is still possible to use a different way of validating instead of .matches with this regex, btw. But perhaps I'm mistaken. – Kevin Cruijssen – 2017-07-04T14:29:56.423

1No, you're totally right! It's doable like this: s->{s[0]-=s[0]<88?0:40;return"TRWAGMYFPDXBNJZSQVHLCKE".charAt(new Integer(new String(s))%23);} for only 94 bytes (with s being a char[]) :p – Olivier Grégoire – 2017-07-04T14:35:38.610

1Or if you want to be complete about the validation: s[0]<88&s[0]>90 for 8 more bytes. – Olivier Grégoire – 2017-07-04T14:50:46.873

@OlivierGrégoire Thanks. And I was indeed looking for full validation after your first comment. I noticed a test case like [5555555 was still validating and incorrectly returning Q. But with the added |s[0]>90 (not & btw :D) it works. Thanks again. – Kevin Cruijssen – 2017-07-04T15:32:28.903

Sorry for mixing the operators: I wrote that in a hurry. At least you're properly parsing the idea and not what I write :-P Anyways... I guess it's probably the only time where an answer with new Integer(new String(...)) is shorter than any other Java answer (so far)! – Olivier Grégoire – 2017-07-04T16:32:25.527

2

PHP, 88 bytes

prints 1 for an error

$a[0]=strtr(($a=$argn)[0],ZYX,210);echo!ctype_digit($a)?:TRWAGMYFPDXBNJZSQVHLCKE[$a%23];

Try it online!

Jörg Hülsermann

Posted 2017-07-03T06:18:57.953

Reputation: 13 026

2

Jelly, 42 bytes

“X0Y1Z2”⁸y@1¦RṪ€V%23ị“Ñ×ⱮEɼiʋ}'uƒẹsø’ṃØA$¤

Try it online!

Too long, Jelly! Dennis is disappointed of you![citation-needed]

Erik the Outgolfer

Posted 2017-07-03T06:18:57.953

Reputation: 38 134

1

q/kdb+, 68 bytes

Solution:

{"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}

Examples:

q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"00000010"
"X"
q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"01234567"
"L"
q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"98765432"
"M"
q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"69696969"
"T"
q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"42424242"
"Y"
q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"Z5555555"
"W"
q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"Y0000369"
"S"
q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"A1234567"
" "
q){"TRWAGMYFPDXBNJZSQVHLCKE"mod["J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x;23]}"1231XX12"
" "

Explanation:

If the first character, x 0, is in the string "XYZ" then a will be 0, 1 or 2. If the first character is not in the string, then a will be 3. If a is less than 3, we switch out the first character for the string of a (0, 1 or 2), otherwise we switch out for the first character (thus effectively doing nothing). This string is cast to a long ("J"$), which is then mod'd with 23 to give the remainder. This remainder is used to index into the lookup table.

{ "TRWAGMYFPDXBNJZSQVHLCKE" mod["J"$$[3>a:"XYZ"?x 0;string a;x 0],1_x;23] } / ungolfed solution
{                                                                         } / lambda function
                            mod[                                     ;23]   / performds mod 23 of the stuff in the gap
                                                                  1_x       / 1 drop input, drops the first character
                                                                 ,          / concatenation
                                    $[             ;        ;   ]           / if COND then TRUE else FALSE - $[COND;TRUE;FALSE]
                                        a:"XYZ"?x 0                         / "XYZ" find x[0], save result in a
                                      3>                                    / is this result smaller than 3
                                                    string a                / if so, then string a, e.g. 0 -> "0"
                                                             x 0            / if not, just return first character x[0]
                                "J"$                                        / cast to long
  "TRWAGMYFPDXBNJZSQVHLCKE"                                                 / the lookup table

Notes:

" " is returned in the error scenarios, this is because the cast returns a null, and indexing into a string at index null is an empty char. I could add 4 bytes at the beginning ("!"^) to make it more obvious that an error had occurred:

q){"!"^"TRWAGMYFPDXBNJZSQVHLCKE"("J"$$[3>a:"XYZ"?x 0;($)a;x 0],1_x)mod 23}"1231XX12"
"!"

streetster

Posted 2017-07-03T06:18:57.953

Reputation: 3 635

1

JavaScript(ES6), 121 byte

f=i=>{c=+i[0];a=3;while(a--){i[0]=="XYZ"[a]&&(c=a)}b=7;while(b--){c= +i[7-b]+c*10}return "TRWAGMYFPDXBNJZSQVHLCKE"[c%23]}

console.log([f("00000010"),f("01234567"),f("98765432"),f("69696969"),f("42424242"),f("Z5555555"),f("Y0000369"),f("A1234567"),f("1231XX12")])

Евгений Новиков

Posted 2017-07-03T06:18:57.953

Reputation: 987

1

Japt, 50 bytes

Similar to most of the other approaches.

Input and output is lowercase, outputs undefined for invalid input.

`tr°gmyfpdxbnjzsqvhlcke`g("xyz"øUg)?Uc %4+UÅ:U %23

Test it
Test all valid test cases

Shaggy

Posted 2017-07-03T06:18:57.953

Reputation: 24 623

1

Rust, 206 bytes

I don't think rust is well suited for code golfing -_-

let b=|s:&str|{s.chars().enumerate().map(|(i,c)|match i{0=>match c{'X'=>'0','Y'=>'1','Z'=>'2',_=>c},_=>c}).collect::<String>().parse::<usize>().ok().and_then(|x|"TRWAGMYFPDXBNJZSQVHLCKE".chars().nth(x%23))};

dgel

Posted 2017-07-03T06:18:57.953

Reputation: 91

1

05AB1E, 41 40 39 bytes

ć…xyz2ÝJ‡ìDd_i.ǝ}23%.•Xk¦fΣT(:ˆ.Îðv5•sè

Takes the input in lowercase (to save 1 byte yay)

Try it online!

Prints the input to STDERR if it is malformed

Explanation

ć…xyz2ÝJ‡ìDd_i.ǝ}23%.•Xk¦fΣT(:ˆ.Îðv5•sè
ć                                       # Get head of input and put the rest of the input under it on the stack
 …xyz                                   # Push xyz
     2ÝJ                                # Push 012
        ‡                               # Transliterate
         ì                              # Prepend to the rest of the input
          Dd_                           # Does the result contain something other than numbers?
             i.ǝ}                       # If so print input to STDERR
                 23%                    # Modulo 23
                    .•Xk¦fΣT(:ˆ.Îðv5•   # Pushes the character list
                                     sè # Get the char at the index of the modulo

Datboi

Posted 2017-07-03T06:18:57.953

Reputation: 1 213

0

Dyalog APL, 95 bytes

{'TRWAGMYFPDXBNJZSQVHLCKE'[1+23|(10⊥¯1+'0123456789'⍳{(⍕{('XYZ'⍳⍵)<4:('XYZ'⍳⍵)-1⋄⍵} ⊃⍵),1↓⍵}⍵)]}

This is a monadic operator that accepts a character string as its operand and returns its result.

FIXME it doesn't check its input. It's not properly golfed.

Usage:

    OP ← {'TRWAGMYFPDXBNJZSQVHLCKE'[1+23|(10⊥¯1+'0123456789'⍳{(⍕{('XYZ'⍳⍵)<4:('XYZ'⍳⍵)-1⋄⍵} ⊃⍵),1↓⍵}⍵)]}

      OP '01234567'
L

      OP '00000010'
X

Locoluis

Posted 2017-07-03T06:18:57.953

Reputation: 741