loading...

Daily Challenge #282 - Car License Plate Calculator

thepracticaldev profile image dev.to staff ・2 min read

The car registering system of a city assigns two types of numbers:

The Customer ID - it's a natural number between 0 and 17558423 inclusively, which is assigned to the car buyers in the following order: the first customer receives ID 0, the second customer receives ID 1, the third customer receives ID 2, and so on.

Number Plate - assigned to the car and contains the series ( three Latin letters from ‘a’ to ‘z’) and the serial number (three digits from 0 to 9).
Example: aaa001. Each Number Plate is related to the given Customer ID. For example: Number Plate aaa000 is related to Customer ID 0; Number Plate aaa001 is related to Customer ID 1, and so on.

Write a function findPlate which takes the Customer ID as an argument, calculates the Number Plate corresponding to this ID and returns it as a string, considering the following:

The serial numbers start at 001, end at 999.

The series change once the serial number hits 999. The left letter changes first, alphabetically:

aaa001...aaa999, 
baa001...baa999,
...... , 
zaa001...zaa999

As soon as the left letter reaches z and the series z** has moved through all the serial numbers the middle letter changes alphabetically as follows:

aba001...aba999, 
bba001... bba999, 
cba001...cba999, 
......, 
zba001...zba999, 
aca001...aca999, 
bca001...bca999,

As soon as the middle letter and the left letter both reaches z and the series zz* has moved through all its serial numbers, the right letter changes alphabetically as follows:

aab001...aab999, 
bab001...bab999, 
cab001...cab999, 
......, 
zab001...zab999, 
abb001...abb999, 
bbb001...bbb999, 
cbb001...cbb999, 
......,
zbb001...zbb999, 
acb001...acb999, 
......, 
zzz001...zzz999

When all the possible series and their serial numbers are exhausted the last Number Plate will be zzz999.

Notes:

  • No spaces are allowed between the characters in the returned string. So 'abc123' is valid, but 'a b c 1 2 3' is not.
  • If the serial number has less than 3 digits, the missing places should be filled with zeroes. Example: serial number '12' should equal '012'; serial number '4' should equal '004'. Customer IDs starts with 0.

Tests

findPlate(3)
findPlate(1487)
findplate(17558423)
findPlate(234567)

Good luck!


This challenge comes from William_2004 on CodeWars. Thank you to CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge idea for a future post? Email yo+challenge@dev.to with your suggestions!

Posted on by:

thepracticaldev profile

dev.to staff

@thepracticaldev

The hardworking team behind dev.to ❤️

Discussion

pic
Editor guide
 

letters = 'abcdefghijklmnopqrstuvwxyz'
def find_the_number_plate(n):
    # As serial numbers go from 001 to 999, we take n % 999 and convert it to a 0 padded string
    ser = str(n%999 + 1).rjust(3, '0')
    # To find the letters, we need to divide by 999 and then repeatedly divide by 26 to get the 3 letters
    l = n//999
    return letters[l%26]+letters[(l//26)%26]+letters[(l//(26*26))%26] + ser

 

The problem statement contradicts itself.

First it says that "Number Plate aaa000 is related to Customer ID 0; Number Plate aaa001 is related to Customer ID 1, and so on."

Then it says "The serial numbers start at 001, end at 999", and goes on to show lists of plates with serial numbers in the format xxx001..xxx999.

Together, they mean that number plate aaa000 both does and does not exist.

 

Here is the simple solution with Python to create mapping letter lists to complete this challenge :).

def find_the_number_plate(customer_id):
    letter_string = 'abcdefghijklmnopqrstuvwxyz'
    letters = [
        'aa',
        'ab',
        'ac',
        'ad',
        'ae',
        'af',
        'ag',
        'ah',
        'ai',
        'aj',
        'ak',
        'al',
        'am',
        'an',
        'ao',
        'ap',
        'aq',
        'ar',
        'as',
        'at',
        'au',
        'av',
        'aw',
        'ax',
        'ay',
        'az',
    ]
    map_letters = []
    for letter in letters:
        for second_index in list(range(1, 26)):
            for index in list(range(0, 26)):
                map_letters.append(letter_string[index] + letter)
            letter = letter_string[second_index] + letter[-1]
        for index in list(range(0, 26)):
            map_letters.append(letter_string[index] + letter_string[-1] + letter[-1])

    number_str = str(customer_id - int(customer_id / 999) * 999 + 1)
    number_str = '0' * (3 - len(number_str)) + number_str 

    map_str = map_letters[int(customer_id / 999)]

    return map_str + number_str
 

With the use UTF-8 table

Lowercase alphabet position 97-122
For the 'abz099' Result:

addNum=1000(01+126+25262) addNum = 1000 * (0 * 1 + 1 * 26 + 25 * 26 ^ 2)

And add 99

def find_plate(number_plate):
    byte_array = number_plate.encode(encoding="utf-8")
    add_num = byte_array[0] - 97 + (byte_array[1] - 97) * 26 + (byte_array[2] - 97) * (26 ** 2)
    return 1000 * add_num + int(byte_array[3:6])
 
find_plate(CustomerId, NumberPlate) :-
  var(NumberPlate),
  divmod(CustomerId, 1000, Q1, R1),
  divmod(Q1, 26, Q2, R2),
  divmod(Q2, 26, R4, R3),
  R1Plus is R1 + 1000,
  atom_chars(R1Plus, [_ | Digits]),
  C2 is R2+97,
  C3 is R3+97,
  C4 is R4+97,
  atom_chars(Letters, [C4, C3, C2]),
  atomic_list_concat([Letters | Digits], NumberPlate), !.

find_plate(CustomerId, NumberPlate) :-
  atom_codes(NumberPlate, [L1, L2, L3, N1, N2, N3]),
  atom_codes(NA, [N1, N2, N3]),
  atom_number(NA, N),
  CustomerId is 676000*(L1-97)+26000*(L2-97)+1000*(L3-97)+N.
 

I thought to use Elixir since it has built-in base conversion (and not just for the usual powers of two suspects like octal and hex), but theirs assumes the digit sequence is 0..9A..Z, so there is still an impedance mismatch to a..z encoding, and the need (apparently) to bludgeon one's way through the edge cases, particularly 'magic constants' based on knowledge of the ASCII character set.

defmodule FindPlate do
  @doc """
  Generate a number plate from a customer_id
  """
  @spec find_plate(Integer.t()) :: String.t()
  def find_plate(customer_id) do
    {mst, digits} = (customer_id+26*26*26*1000) # kludge
    |> Integer.to_charlist
    |> Enum.split(-3)
    mst = if(mst == [], do: '0', else: mst) # kludge
    {_, letters} = mst
    |> List.to_integer
    |> Integer.to_charlist(26)
    |> Enum.map(fn c -> if(c <= ?9, do: c+?a-?0, else: c+42) end)
    |> Enum.split(-3) # counterkludge
    letters++digits
  end
end
 

APL (using Dyalog APL):

findPlate←{
  ⎕IO←0
  values←26 26 26 999⊤⍵
  (alpha digit)←(3↑values)(⊢/values)
  (⎕UCS 97+⌽alpha),⎕D[(3⍴10)⊤1+digit]
}

(Use the bookmarklet on this post to see the code with APL font and syntax highlighting.)

Demo can be found here.

Explanation:

findPlate←{
  ⍝ Set 0-based indexing for ⎕D later
  ⎕IO←0
  ⍝ Mixed base decomposition for 999 numbers and
  ⍝ three alpha places
  values←26 26 26 999⊤⍵
  ⍝ Extract alphabet part and number part
  (alpha digit)←(3↑values)(⊢/values)
  ⍝ Extract three digits of number, index into
  ⍝ ⎕D ('0123456789'), and concatenate to
  ⍝ reversed alphabet part (using Unicode codepoint)
  (⎕UCS 97+⌽alpha),⎕D[(3⍴10)⊤1+digit]
}
 

In JS with toString and a custom radix ended up being pretty damn unwieldy...

const 
  // adding aMinusZero to a charCode converts a digit to a
  //   lowercase letter (i.e. 0 -> 'a')
  aMinusZero = 'a'.charCodeAt(0) - '0'.charCodeAt(0),
  // radix26ToLatin converts the given character from
  //    radix 26 (0-9, a-p) to a letter(a-z)
  radix26ToLatin = c =>
    String.fromCharCode(
    c.charCodeAt(0) 
    + (c.charCodeAt(0) < 'a'.charCodeAt(0) ? aMinusZero : 10)
  ),
  // numberToLatin converts a number to its a-z representation
  numberToLatin = i =>
    Number(i).toString(26).replace(/./g, radix26ToLatin),

  // plateNumberForCustomerId wraps things up with an
  //    uncomfortably complicated bow 🤷‍♂️
  plateNumberForCustomerId = id =>
    [...numberToLatin(Math.floor(id / 999)).padStart(3, 'a')]
      .reverse()
      .join('') 
    +
    String((id % 999) + 1)
      .padStart(3, 0);

Testn...

console.table(
    Object.fromEntries(
        [3, 1487, 40000, 17558423, 234567].map(
          i=> [i, plateNumberForCustomerId(i)]
        )
    )
)
(index) value
3 "aaa004"
1487 "baa489"
40000 "oba041"
234567 "aja802"
17558423 "zzz999"

seems to agree with the kata test cases

Advantage: avoids hardcoding the number of letters and digits (they could be brought out as params easily)
Disadvantages: all the other things

 

Seems to be working -
letters = 'abcdefghijklmnopqrstuvwxyz'

def find_the_number_plate(n):
# As serial numbers go from 001 to 999, we take n % 999 and convert it to a 0 padded string
ser = str(n%999 + 1).rjust(3, '0')
# To find the letters, we need to divide by 999 and then repeatedly divide by 26 to get the 3 letters
l = n//999
return letters[l%26]+letters[(l//26)%26]+letters[(l//(26*26))%26] + ser