Introduction

Many old chess books use descriptive notation to notate squares on a chessboard, naming the file (column) of the square based on the starting piece positions of White or Black, depending on whose move it is. The ranks (rows) are numbered 1 through 8, with 1 being closest to the player. For example, from White's perspective, QR2 represents the file where the queenside rook started, on the 2nd rank from the bottom.

However, this system has been entirely replaced by algebraic notation in the past few decades, which simply notates files with letters a-h and rows with numbers 1-8, always "from White's point of view". Therefore it would be convenient to have a program that can translate squares from old books.

Note: This problem is the simplified inverse of Convert Algebraic notation to Descriptive notation

Diagrams

Descriptive notation

Credit: David Eppstein

Algebraic notation

Credit: ILA-boy, Klin, Beao

Given a chess square as a string and either white or black's perspective in descriptive notation, output the chess square in algebraic notation. Reasonable inputs may be used, such as a boolean for white/black perspective. For simplicity, we do not handle moves (including captures) or special symbols (promotion, castling, blunders, end of game, etc.)

The inputs and outputs are case sensitive and follow the above diagrams, with descriptive notation always using capital letters and algebraic notation always using lowercase letters. You must take the input square as one string like "QR1", not two like "QR" and "1".

Test cases

white,QR1 => a1
white,KR8 => h8
white,Q4  => d4
white,KN7 => g7
black,QR1 => a8
black,KR8 => h1
black,Q4  => d5
black,KN7 => g2


JavaScript (ES6), 62 bytes

Expects (white)(string), where white is a Boolean flag.

w=>s=>"egfbchda"[(q=parseInt(s,b=29))/b%43%10|0]+(w?q%b:9-q%b)


Try it online!

How?

We parse the descriptive notation as a base-29 string and save the result in $$\q\$$.

The row number is $$\q\bmod29\$$ for White and $$\9-(q\bmod29)\$$ for Black.

For the column, we apply this minimal perfect hash function:

$$\big(\lfloor q/29\rfloor\bmod43\big)\bmod10$$

Column String $$\\lfloor q/29 \rfloor\$$ $$\\bmod 43\$$ $$\\bmod 10\$$
a "QRx" 781 7 7
b "QNx" 777 3 3
c "QBx" 765 34 4
d "Qx" 26 26 6
e "Kx" 20 20 0
f "KBx" 591 32 2
g "KNx" 603 1 1
h "KRx" 607 5 5

Example search code

This simple piece of code looks for the shortest valid hash functions of the form parseInt(s, b) % m0 % m1 with $$\28\le b\le 36\$$ and $$\2\le m_1.

It is quite inefficient because it tries all tuples $$\(b,m_0,m_1)\$$ even if $$\(b,m_0)\$$ alone leads to an unrecoverable lookup collision. But this works fine for a such a limited search space.

const O = {
"QR" : "a",
"QN" : "b",
"QB" : "c",
"Q"  : "d",
"K"  : "e",
"KB" : "f",
"KN" : "g",
"KR" : "h"
};

let best = 1 / 0;

for(let b = 28; b <= 36; b++) {
for(let m0 = 2; m0 <= 100; m0++) {
for(let m1 = 2; m1 < m0; m1++) {
let lookup = Array(m1).fill('?');

if(Object.keys(O).every(key => {
let i = parseInt(key, b) % m0 % m1;
if(lookup[i] == '?') {
lookup[i] = O[key];
}
return lookup[i] == O[key];
})) {
let str = lookup.join('').replace(/\?*$/, ''), code = "${str}"[parseInt(s,${b})%${m0}%${m1}], score = code.length; if(score <= best) { best = score; console.log(Score:${score} : \${code});
}
}
}
}
}


Try it online!

• Perfect hash strikes again
– qwr
Jul 13 at 8:03
• How do you find these hash functions? Jul 15 at 18:42
• @Adam Basically, the formula shape is handcrafted and the parameters are brute-forced. I've added some example search code. Jul 16 at 10:47
• @Arnauld you have a pre-existing hash searcher for tables right?
– qwr
Jul 17 at 4:49
• @qwr What do you mean by tables? Jul 17 at 11:41

JavaScript, 68 bytes

s=>w=>'dcbaefgh'[x=s.at(-1),('P'>s)*4-~'BNR'.search(s[1])]+(w?x:9-x)


f=

s=>w=>'dcbaefgh'[x=s.at(-1),('P'>s)*4-~'BNR'.search(s[1])]+(w?x:9-x)

function test(white) {
const table = document.createElement('table');
for (const i of (white ? '12345678' : '87654321')) {
const row = table.appendChild(document.createElement('tr'));
for (const j of 'QR,QN,QB,Q,K,KB,KN,KR'.split(',')) {
row.appendChild(document.createElement('td')).textContent = f(j + i)(white);
}
}
document.body.appendChild(table);
}

test(true);
test(false);
table { display: inline-table; margin-right: 30px; border-collapse: collapse; border: 1px solid black; }
td { width: 25px; height: 25px; line-height: 25px; text-align: center; background: white; color: black; }
tr:nth-child(odd) td:nth-child(even), tr:nth-child(even) td:nth-child(odd) { filter: invert(); }

• Nice table output! Do you need f= at the start? Does an anonymous function work?
– qwr
Jul 13 at 2:25
• @qwr Edited. Forgot to remove after some code changes...
– tsh
Jul 13 at 2:30
• Is it a curried function now? I assume so since it matches the syntax you would use for some other FP languages
– qwr
Jul 13 at 2:31

Python, 65 bytes

lambda t,s:f'{97+((x:=int(s[1:],30))>>8^3+(s<"Q")):c}{(-t^x)%10}'

Attempt This Online!

Old Python, 67 bytes (@xnor)

lambda t,s:f'{97+(11*ord(s[1])>>7^7*(s>"L")):c}{8**t*int(s[-1])%9}'

Attempt This Online!

Old Python, 68 bytes (@ovs)

lambda t,s:f'{97+(11*ord(s[1])>>7^7*(s>"L")):c}{int(t*"-"+s[-1])%9}'

Attempt This Online!

Old Python, 72 bytes

lambda t,s:"%c%i"%(97+("RNB".find(s[1])%4^7*(s<"Q")),int(t*"-"+s[-1])%9)

Attempt This Online!

A golfed version of @Adam's answer which I was advised is different enough to warrant a separate post.

• An f-string can save a byte, and using a magic formula instead of str.find brings this down to 68 bytes
– ovs
Jul 13 at 13:25
• @ovs Neat magic! Are you sure you don't want to submit your own answer? Jul 13 at 13:36
• No I just entered the numbers into pysearch, don't think this is really worth an additional answer. But feel free to not include that change if you want to keep your original formula.
– ovs
Jul 13 at 13:41
• @ovs No, no. I actually like it. Jul 13 at 13:52
• 8**t*int(s[-1])%9 saves 1, though I like your trick with the - in the int()
– xnor
Jul 17 at 9:44

Jelly, 24 22 bytes

ṪVN³¡%9Ṿṭ“¡ṭ."“¬’ḥịØaƲ


Try it online! (the footer is a test suite that tests all 64 possibilities from a given player's point of view)

Function submission. Takes the coordinates in descriptive notation as a function argument, and the viewpoint (white=0, black=1) as a command-line argument.

Explanation

ṪVN³¡%9Ṿṭ“¡ṭ."“¬’ḥịØaƲ
Ṫ                       On the last character of the input,
V                      convert it to a number,
N                     negate
¡                   a number of times
³                    equal to the command-line argument,
%9                 wrapping-modulo by 9,
Ṿ                convert back to a string,
ṭ               and prepend
ḥ      {the result of} the randomly generated function
“¡ṭ."“           #29699285
“¬’         with codomain [1..8]
Ṫ                    Ʋ    applied to the input minus its last character, after
ịØa     indexing into the lowercase alphabet


The probability that a randomly generated function with an 8-element codomain will map the eight column names onto eight different outputs is approximately 1 in 416. As such, it's possible to convert the column names from descriptive to algebraic notation simply by searching through Jelly's catalogue of random functions to find one that's a perfect hash function for the given inputs (i.e. it maps each of them to a different number from 1 to 8), and specify the codomain using an 8-character string that contains the desired output column names in the order with which the function generates them. Function #2423 happens to have the desired behaviour (I simply checked random functions until I found one that worked).

However, doing this requires the column names to be given explicitly. It's golfier to express them by indexing into the alphabet, but this requires finding a function that maps each of the eight column names onto a different number from 1 to 8 in the correct order. The odds of any given randomly generated function happening to do this are 1 in 16777216. After searching through tens of millions of randomly generated functions, I discovered that function #29699285 happens to have the desired behaviour.

When an appropriate function for converting column names is available, the only remaining requirement is to remove the row number before running the function, subtract from 9 if necessary, and add it back at the end. (The conditional subtraction from 9 is done by conditionally negating it, and then taking its value modulo 9.)

Batch Script, 179 Bytes

SET t=%2
SET /A a=96,p=%t:~-1%
FOR %%? in (QR QN QB Q K KB KN KR)DO SET /A a+=1,n=9-(((9-p)*(1-%1))+p*%1)&IF %%? == %t:~0,-1% CMD /C EXIT %%a%%&CALL ECHO %%=exitcodeAscii%%%%n%%


This is assuming @ECHO OFF is enabled. The second arg is the string (ie QR1), and 1st is the side. 0 for white, 1 for black. For example on CMD, if file is named chess.bat

CALL chess.bat 0 QR1


returns a1. I use the built-in variable =exitcodeAscii to get the letters from a-h, the rest is just a simple FOR loop.

• Welcome to Code Golf, and nice answer! Jul 13 at 0:56
• I'm not sure what the standard is for batch script IO. are there ways to use stdin args or write functions?
– qwr
Jul 13 at 1:41
• Actually that can shave off 2 bytes by putting the args in stdin, so I guess it doesn't matter lol Jul 13 at 2:04
• Why are there 3 args? what is "position"?
– qwr
Jul 13 at 2:23
• You have to take the whole square string as one string, ex. "QR1" not "QR" and "1"
– qwr
Jul 13 at 2:29

Python, 85 bytes

lambda t,s:"%c%i"%(101-(q:="Q"in s)+(2*q-1)*~"BNR".find(s[1]),9*t-(2*t-1)*int(s[-1]))


Attempt This Online!

Takes in the integer 0 if it's white's move, 1 if it's black's move, along with the notation string. Returns a string.

Explanation

Not much of an explanation, but here's the code separated into a series of steps:
def f(t,s):
q=0+("Q"in s)
r="BNR".find(s[1])
u=(2*q-1)*~r
col=100-(q-u)
x=(2*t-1)
row=9*t-x*int(s[-1])
return "%c%i"%(col,row)

• abs(9*t-int(s[-1])) is a bit shorter for the row Jul 13 at 2:05
• @loopywalt the answer is sufficiently different to be a new answer (with credit to this answer)
– qwr
Jul 13 at 2:21
• @qwr hm, if you say so ;-) Jul 13 at 2:42

Retina 0.8.2, 49 bytes

TdRdb.+
TQKl,d-e_
+Tb-gBNRa-cf-h_BN.[A-Z]


Try it online! Link includes test cases. Explanation:

TdRdb.+


For black, subtract the digit from 9, i.e. transliterate 0-9 to 9-0.

TQKl,d-e_


Q becomes d, K becomes e, and delete the white,/black, indicator.

+Tb-gBNRa-cf-h_BN.[A-Z]


For each column away from the centre, update the letter, e.g. QRdRcNbBa.

Batch, 126 bytes

@set s=%2
@set/ar=(%1*7^^%s:~-1%-1)+1
@for %%i in (QR.a QN.b QB.c Q.d K.e KB.f KN.g KR.h)do @if %%~ni==%s:~,-1% echo%%~xi%r%


Takes a boolean (0/1) and a string as command-line parameters (-2 bytes by abusing the input format to take 0 or 7 as the first parameter). Explanation:

@set s=%2


Get the second parameter into a variable so that we can slice it.

@set/ar=(%1*7^^%s:~-1%-1)+1


Calculate the descriptive row from the algebraic row by XORing the 0-indexed row with 0 or 7 as appropriate.

@for %%i in (QR.a QN.b QB.c Q.d K.e KB.f KN.g KR.h)do @if %%~ni==%s:~,-1% echo%%~xi%r%


Based on my answer to the linked challenge, loop over the pairs of descriptive and algebraic columns and output the algebraic column corresponding to the descriptive column. Note that echo ignores most punctuation immediately after the o which I use to consume the . that's part of the ~x extension, saving 2 bytes.

05AB1E, 29 bytes

Çнθ<…BNR¹Åsk+.•7œΔ&‘•èI9*¹θα«


Inputs in the order notation,perspective, where the perspective is 1/0 for black/white respectively.

Explanation:

Ç           # Convert the (implicit) first input-string to a list of codepoint-integers
н          # Leave just the first codepoint-integer
θ         # Leave just its last digit
<        # Decrease it by 1:
#  0 if 'Q' and 4 if 'K'
…BNR        # Push string "BNR"
¹       # Push the first input-string again
Ås     # Pop and leave just its middle character
# (if the input contains just two characters, those will be the 'middle')
k    # Get the index of this middle in the string (or -1 if not found):
#  0 if 'B'; 1 if 'N'; 2 if 'R'; -1 if 'Q#'/'K#'
+           # Add the two integers together
.•7œΔ&‘•   # Push compressed string "cbaefghd"
è  # Index the earlier integer into this string
# (0-based modular, so the -1 indexes into the trailing "d")
I           # Push the second input-digit
9*         # Multiply it by 9
¹        # Push the first input-string yet again
θ       # Pop and leave its last character (the digit)
α      # Get the absolute difference of it with the (9*perspective)
«     # Append this digit to the earlier character
# (after which the result is output implicitly)


See this 05AB1E tip of mine (section How to compress strings not part of the dictionary?) to understand why .•7œΔ&‘• is "cbaefghd".

Python 3.10, 82 bytes

lambda b,s:C[C.find(s[:-1])-1]+str(abs(9*b-int(s[-1])))
C="dQeKaQRbQNcQBfKBgKNhKR"

Attempt This Online!

Testing out dictionary interleaving idea which is a little shorter than converting to ascii codes with chr. "Q" and "K" have to come first, otherwise they will match for at "QR".

-4 bytes using find instead of index and a better rank calculation for white/black.

Charcoal, 35 bytes

§β⌕⪪”＆↓↶Ｇ«ü\~6ε⪪θ⊞↷” …η⊖ＬηＩ⎇Ｎ⁻⁹ΣηΣη


Try it online! Link is to verbose version of code. Takes the White/Black flag as a boolean. Explanation:

§β⌕⪪”＆↓↶Ｇ«ü\~6ε⪪θ⊞↷” …η⊖Ｌη


Look up the column in a compressed table and output the lowercase letter with that index.

Ｉ⎇Ｎ⁻⁹ΣηΣη


Subtract the row from 9 if the player is Black.