7
\$\begingroup\$

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]}
end  

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

\$\endgroup\$
  • 1
    \$\begingroup\$ out of curiosity, why not [0,0] for "a1" ? \$\endgroup\$ – icy Sep 15 '14 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\$ – O.F. Sep 16 '14 at 7:47
10
\$\begingroup\$
  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])$/
  end.compact
end

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] }
end

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
|improve this answer|||||
\$\endgroup\$
  • \$\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\$ – O.F. Sep 11 '14 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\$ – Jörg W Mittag Sep 11 '14 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\$ – Jörg W Mittag Sep 11 '14 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\$ – Jörg W Mittag Sep 11 '14 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 '14 at 14:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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