Given a chess coordinate as a string (e.g. "a1") I'd like to transform it into a 2-D array (so, for "a1" I'd like to get [1,1]).

Here's what I came up with:

def safe_pawns(pawns)
  pawns.inject([]){|res, pwn| res << [pwn.split('')[0].index(/[a-h]/) + 1, pwn.split('')[1].to_i]}

Can anyone suggest a refactoring please to make it more idiomatic Ruby?

  • 1
    \$\begingroup\$ out of curiosity, why not [0,0] for "a1" ? \$\endgroup\$
    – icy
    Sep 15, 2014 at 21:58
  • \$\begingroup\$ Purely out of habit. I learned ROT13 way before I learned of coding. :) I can see why you ask: why have a "+1" in the mapping of the letter character into the array... and you are absolutely right! Like I said, force of habit. \$\endgroup\$
    – user52901
    Sep 16, 2014 at 7:47

1 Answer 1

  1. Give your code some breathing room. I.e. ) { |a, b| instead of ){|a, b|. And there's no need to put everything on one line. The way your block works right now, it'd be better to store the result of pwn.split in a variable, instead of calling split twice. (There's also no need to abbrevate "pawn" as "pwn".)

  2. That being said, strings support array-like access, so you don't need the split at all.

  3. Judging from your code, pawns is an array. Transforming an n-element array to a new n-element array is called "mapping". You're using inject which is also known as reduce (and fold in many other languages; see comments) - an operation most often used to take an n-element array and reduce it to a single value. So step 1: Use map instead of inject.

  4. I'd probably use a regex to pull the string apart. It'll double as a way to check the coordinate strings for validity (e.g. so no "z9" coordinates will slip through).

  5. It's a little low-level, but we can use the fact that "a" is 97 in ASCII. So to get the number for a letter, we can say letter.ord - 96.

You get something like this:

def safe_pawns(squares)
  squares.map do |square|
    [$1.ord - 96, $2.to_i] if square.downcase =~ /^([a-h])([1-8])$/

Alternatively, if you're sure that all the input coordinates are valid, lowercase strings already, you don't need the regex or the downcasing:

def safe_pawns(squares)
  squares.map { |square| [square[0].ord - 96, square[1].to_i] }

As tokland points out in the comments, we can avoid the hardcoded 96 (which isn't very self-explanatory) and instead get the letter-to-number translation by saying:

square[0].ord - 'a'.ord + 1
  • \$\begingroup\$ Wow! Lots of helpful tips ("teach a man to fish") an their final accumulation ("give them a fish"). Not to mention a non-textbook, one-liner explanation on what inject is. Thank you very, very much. Can't upvote yet 'cause I don't have enough cred, but this sure is the accepted answer by me. \$\endgroup\$
    – user52901
    Sep 11, 2014 at 11:06
  • 1
    \$\begingroup\$ Note that in "reducing to a single value", the "single value" can be arbitrarily complex … like another array, for example. In fact, reduce is universal: everything you can do with iteration aka each (and that means everything you can do with any of the methods in Enumerable), you can do with reduce. There is a proof sketch on the Wikipedia page for fold, and as a proof-of-concept I once re-implemented much of Enumerable on top of reduce instead of each. \$\endgroup\$ Sep 11, 2014 at 12:53
  • 1
    \$\begingroup\$ It becomes much clearer if you stop thinking of reduce as "iterating" and the first argument as an "accumulator" or "starting value". Instead think of it like this: when going through a collection there is either a next element or there isn't. reduce takes two arguments, telling it what to when there is a next element (the block) and there isn't (the zero). Therefore, we have covered all possible cases. \$\endgroup\$ Sep 11, 2014 at 12:57
  • 1
    \$\begingroup\$ Another possible way is to think of it as an interpreter: a collection is a program with two kinds of commands, a stop command and a parametric do command. reduce interprets that program, and it allows you to specify how to interpret the stop command (the zero) and the do command (the block). Note that the opposite is also true, by the way: a compiler or interpreter is really just a reduce of an abstract syntax tree! \$\endgroup\$ Sep 11, 2014 at 13:00
  • 2
    \$\begingroup\$ Two minor notes: 1) In chess the coordinates are called "squares". 2) To avoid "magic numbers" I'd write: square[1].ord - 'a'.ord + 1. \$\endgroup\$
    – tokland
    Sep 13, 2014 at 14:44

Your Answer

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