18

I would like to generate a sequence of letters i.e. "A", "DE" "GJE", etc. that correspond to a number. The first 26 are pretty easy so 3 returns "C", 26 returns "Z", and 27 would return "AA", 28 "AB", and so on.

The thing I can't quite figure out is how to do this so it will handle any number passed in. So if I pass in 4123 I should get back some combination of 3 letters since (26 * 26 * 26) allows for up to +17,000 combinations.

Any suggestions?

rmontgomery429
  • 14,660
  • 17
  • 61
  • 66
  • 2
    This basically is a base-26 number (but without digits in the base) – Sergio Tulentsev Jan 31 '13 at 18:11
  • @SergioTulentsev - This problem is more complex, it turns out. I think the key characteristic that leads to trouble is that `A` means "0" in some cases (as the "ones" digit) but `A` means "1" in other cases (as the "tens/twenty-sixes" digit. Funny to think though that someone at Microsoft had to solve this ;) – Andrew Cheong Jan 31 '13 at 19:04
  • I'm missing something. Why is "A" special? A=1*1=1, AA=1*26+1*1=27. – glenn mcdonald Jan 31 '13 at 21:15
  • @glennmcdonald "A" is not special. "A" always corresponds to "1". What is special is that "0" is skipped. Imagine in decimal, counting up, skipping "0": 1, 2, ..., 8, 9, 11, 12, ..., 18, 19, 21, 22, ..., 98, 99, 111, 112, ... Now, if you do that in heptabidecimal (base 27) instead of decimal, you get the sequence. – sawa Jan 31 '13 at 23:20
  • Oh, duh, right. Probably easier to do it in base 26 for real, with A as 0. Not clear in the question whether the zeroless thing is a requirement or an artifact of the imagined solution. – glenn mcdonald Feb 01 '13 at 03:46
  • @glennmcdonald That won't work. If you consider "A" as "0", then you would have to consider there are arbitrary length of preceding "A"s in front of any sequence (i.e., "B" = "AB" = "AAB" = ...) just like ("1" = "01" = "001" = ...), but that is not what you want, so your idea does not work. – sawa Feb 01 '13 at 08:42
  • Right, I understand. Up to Ryan whether that's OK or not. If the task is just to turn numbers into letter-sequences, then it's fine to ignore leading As just like it's fine to strip leading 0s in decimal. It wasn't clear to me whether it was a requirement for A to be 1 and AA to be 27, etc., or whether that was just an obvious starting point. – glenn mcdonald Feb 02 '13 at 01:19
  • Sorry if I wasn't clear about that. I was looking for 1 to be A and 27 to be AA, yes. – rmontgomery429 Feb 02 '13 at 04:49
  • Possible duplicate: http://stackoverflow.com/questions/13578555 – sawa Feb 12 '13 at 05:48

10 Answers10

18
class Numeric
  Alph = ("a".."z").to_a
  def alph
    s, q = "", self
    (q, r = (q - 1).divmod(26)); s.prepend(Alph[r]) until q.zero?
    s
  end
end

3.alph
# => "c"
26.alph
# => "z"
27.alph
# => "aa"
4123.alph
# => "fbo"
sawa
  • 165,429
  • 45
  • 277
  • 381
  • I believe this is the only correct solution so far, though I'm not exactly sure what's going on... – Andrew Cheong Jan 31 '13 at 19:02
  • 1
    Totally awesome. I also took away a new piece of insight from this. The way to calculate the total combinations is something like !(26 ^ (n-1)). I'm not a mathematician so this might be completely made up but this would give you (26 ^ 3) + (26 ^ 2) + (26 ^ 1) for three letters, and (26 ^ 2) + (26 ^ 1) for two, etc. – rmontgomery429 Jan 31 '13 at 20:25
  • 2
    What is the best way to use this in a rails application? To include the class in the lib directory and require it? – Jeremy Lynch Jan 24 '14 at 11:46
  • 1
    @JeremyRichards Place it in your `config/initializers/numeric.rb` file – Dex Jan 17 '15 at 04:19
16

A tweak on @sawa original answer for Ruby 2.0 since I couldn't get his to work as is:

class Numeric
  Alpha26 = ("a".."z").to_a
  def to_s26
    return "" if self < 1
    s, q = "", self
    loop do
      q, r = (q - 1).divmod(26)
      s.prepend(Alpha26[r]) 
      break if q.zero?
    end
    s
  end
end

and here it is going in reverse from string to integer:

class String
  Alpha26 = ("a".."z").to_a

  def to_i26
    result = 0
    downcased = downcase
    (1..length).each do |i|
      char = downcased[-i]
      result += 26**(i-1) * (Alpha26.index(char) + 1)
    end
    result
  end

end

Usage:

1234567890.to_s26 
# => "cywoqvj"

"cywoqvj".to_i26  
# => 1234567890

1234567890.to_s26.to_i26
# => 1234567890

"".to_i26
# => 0

0.to_s26
# => ""
Dex
  • 12,527
  • 15
  • 69
  • 90
  • 2
    That's a good catch. I've updated my answer so that anything less than 1 will return an empty string. – Dex Apr 13 '15 at 11:33
  • take note the String#to_i26 method modifies the string it's called on downcasing it. To change that, change `downcase!` to `string = downcase` and `char = self[-i]` to `char = string[-i]` – Arye Eidelman Apr 25 '19 at 20:30
8

Strings do have a succ method so they are usable in a Range. The successor to "Z" happens to be "AA", so this works:

h = {}
('A'..'ZZZ').each_with_index{|w, i| h[i+1] = w } 
p h[27] #=> "AA"
steenslag
  • 79,051
  • 16
  • 138
  • 171
  • Using this method, you cannot just randomly calculate the string for a certain number on the spot, but you have to go through all the numbers up to the target number. – sawa Jan 31 '13 at 20:28
  • Sawa's answer is much better for performance reasons, but if you're going with this response as a quick and dirty method, the following will do it in one line: x = 27; ('A'..'ZZZ').reduce(0) {|i, l| break(l) if (i+1) == x; i+1 } – Kent Mewhort Mar 05 '14 at 18:36
  • 1
    Actually this is faster than @Sawa (17 times according to fruity), provided the hash is re-used. It does so on the cost of using much more memory. – steenslag Mar 05 '14 at 19:06
  • This answer is generating the hash in advance for a fixed range, and is merely referring to it when you need it. My answer generates it for an arbitrary sequence on the spot. They cannot be compared. – sawa Mar 05 '14 at 19:08
4

I liked this answer from: https://stackoverflow.com/a/17785576/514483

number.to_s(26).tr("0123456789abcdefghijklmnopq", "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
Community
  • 1
  • 1
robd
  • 9,646
  • 5
  • 40
  • 59
  • This answer doesn't work. The question states that 27 should return "AA" and this method returns "BB" – Dex May 14 '17 at 11:19
  • Sorry - this answer worked for my case where I didn't care about the exact letters generated, but I agree it's wrong. – robd May 14 '17 at 15:03
  • I ended up using this, as I needed a zero – iheggie Aug 15 '17 at 07:47
2

Using the base conversion method found here. I also changed it for the lack of "0" that we have in this numbering system. End cases have been addressed.

def baseAZ(num)
  # temp variable for converting base
  temp = num

  # the base 26 (az) number
  az = ''

  while temp > 0

    # get the remainder and convert to a letter
    num26 = temp % 26
    temp /= 26

    # offset for lack of "0"
    temp -= 1 if num26 == 0

    az = (num26).to_s(26).tr('0-9a-p', 'ZA-Y') + az
  end

  return az
end

irb I/O:

>> baseAZ(1)
=> "A"
>> baseAZ(26^2 + 1)
=> "Y"
>> baseAZ(26*26 + 1)
=> "ZA"
>> baseAZ(26*26*26 + 1)
=> "YZA"
>> baseAZ(26*26*26 + 26*26 + 1)
=> "ZZA"
Phil
  • 6,686
  • 2
  • 19
  • 25
  • Insightful to notice that this number system "lacks a zero", though now obvious in hindsight. I discovered that OP is looking to convert to [a "Bijective Base-26" system](http://en.wikipedia.org/wiki/Hexavigesimal#Bijective_base_26), and searching by that keyword there appears to be no transliteration-based algorithm for the conversion. I think a simple transliteration-based algorithm is impossible because there will always be a certain single-digit symbol in the no-zeroes system that must map to a two-digit symbol in the zeroes system. – Andrew Cheong Jan 31 '13 at 19:49
2
def letter_sequence(n)
    n.to_s(26).each_char.map {|i| ('A'..'Z').to_a[i.to_i(26)]}.join
end
Tom Close
  • 620
  • 5
  • 12
1

Here is a short performant recursion based solution

class Numeric
  # 'z' is placed in the begining of the array because 26 % 26 is 0 not 26
  Alph = ['z'] + ("a".."y").to_a

  def to_alph

    # if self is 0 or negative return a blank string.
    # this is also used to end the recursive chain of to_alph calls
    # so don't replace this with raising an error or returning anything else

    return '' if self < 1

    # (otherwise) return two concatenated strings:
    # the right side is the letter for self modules 26
    # the left side is comprised of:
    #  1. minus one, because this is not a zero-based numbering system.
    #     therefore, round numbers (26, 52...) should return one digit 'z'
    #     instead of two digits 'aa' or 'ba'.
    #  2. divide by 26 and call to_alph on that.
    #     this repeats recursively for every digit of the final string,
    #     plus once more that will return '' to end the recursion chain.

    return ((self - 1) / 26).to_alph + Alph[self % 26]
  end
end
Arye Eidelman
  • 1,579
  • 16
  • 22
1

('a'..'zzz').to_a[n-1] would cover for the first 18_278.

xxjjnn
  • 14,591
  • 19
  • 61
  • 94
0

Based on sawa's answer, I wanted a method that worked independently, albeit recursively, to achieve the desired outcome:

def num_to_col(num)
  raise("invalid value #{num} for num") unless num > 0
  result, remainder = num.divmod(26)
  if remainder == 0
    result -= 1
    remainder = 26
  end
  final_letter = ('a'..'z').to_a[remainder-1]
  result > 0 ? previous_letters = num_to_col(result) : previous_letters = ''
  "#{previous_letters}#{final_letter}".upcase
end
eazy_beans
  • 512
  • 4
  • 5
0

You can get the numerical position of a character in the alphabet by subtracting 96 from its ordinal, like so:

"a".ord - 96
=> 1

"z".ord - 96
=> 26

You can also fetch the alphabetical character by its numerical position by adding 96, like so:

(1 + 96).chr
=> "a"

(26 + 96).chr
=> "z"
Tom Chapin
  • 3,276
  • 1
  • 29
  • 18