Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Installation

Changes
=======

0.2.0 - added optional checksum
0.1.0 - rename gem to base32-crockford
0.0.2 - ruby 1.9 compatibility
0.0.1 - initial release
Expand All @@ -18,9 +18,11 @@ Usage
=====

#!/usr/bin/env ruby

require 'base32/crockford'

Base32::Crockford.encode(1234) # => "16J"
Base32::Crockford.encode(100**10, :split=>5, :length=>15) # => "02PQH-TY5NH-H0000"
Base32::Crockford.decode("2pqh-ty5nh-hoooo") # => 10**100
Base32::Crockford.encode(1234, checksum: true) # => "16JD"
Base32::Crockford.decode("16JD", checksum: true) # => 1234
45 changes: 34 additions & 11 deletions lib/base32/crockford.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,23 @@ module Base32
#
#
class Base32::Crockford
VERSION = "0.1.0"
VERSION = "0.2.0"

ENCODE_CHARS =
%w(0 1 2 3 4 5 6 7 8 9 A B C D E F G H J K M N P Q R S T V W X Y Z ?)

CHECKSUM_MAP = { "*" => 32, "~" => 33, "$" => 34, "=" => 35, "U" => 36 }

DECODE_MAP = ENCODE_CHARS.to_enum(:each_with_index).inject({}) do |h,(c,i)|
h[c] = i; h
end.merge({'I' => 1, 'L' => 1, 'O' => 0})

# encodes an integer into a string
#
# when +checksum+ is given, a checksum is added at the end of the the string,
# calculated as modulo 37 of +number+. Five additional checksum symbols are
# used for symbol values 32-36
#
# when +split+ is given a hyphen is inserted every <n> characters to improve
# readability
#
Expand All @@ -63,12 +69,14 @@ class Base32::Crockford
#
def self.encode(number, opts = {})
# verify options
raise ArgumentError unless (opts.keys - [:length, :split] == [])
raise ArgumentError unless (opts.keys - [:length, :split, :checksum] == [])

str = number.to_s(2).reverse.scan(/.{1,5}/).map do |bits|
ENCODE_CHARS[bits.reverse.to_i(2)]
end.reverse.join

str = str + ENCODE_CHARS[number % 37] if opts[:checksum]

str = str.rjust(opts[:length], '0') if opts[:length]

if opts[:split]
Expand All @@ -93,17 +101,26 @@ def self.encode(number, opts = {})
# Base32::Crockford.decode("3G923-0VQVS") # => 123456789012345
#
# returns +nil+ if the string contains invalid characters and can't be
# decoded
# decoded, or if checksum option is used and checksum is incorrect
#
def self.decode(string)
clean(string).split(//).map { |char|
def self.decode(string, opts = {})
if opts[:checksum]
checksum_char = string.slice!(-1)
checksum_number = DECODE_MAP.merge(CHECKSUM_MAP)[checksum_char]
end

number = clean(string).split(//).map { |char|
DECODE_MAP[char] or return nil
}.inject(0) { |result,val| (result << 5) + val }

number % 37 == checksum_number or return nil if opts[:checksum]

number
end

# same as decode, but raises ArgumentError when the string can't be decoded
#
def self.decode!(string)
def self.decode!(string, opts = {})
decode(string) or raise ArgumentError
end

Expand All @@ -112,17 +129,23 @@ def self.decode!(string)
#
# replaces invalid characters with a question mark ('?')
#
def self.normalize(string)
clean(string).split(//).map { |char|
def self.normalize(string, opts = {})
checksum_char = string.slice!(-1) if opts[:checksum]

string = clean(string).split(//).map { |char|
ENCODE_CHARS[DECODE_MAP[char] || 32]
}.join

string = string + checksum_char if opts[:checksum]

string
end

# returns false iff the string contains invalid characters and can't be
# returns false if the string contains invalid characters and can't be
# decoded
#
def self.valid?(string)
!(normalize(string) =~ /\?/)
def self.valid?(string, opts = {})
!(normalize(string, opts) =~ /\?/)
end

class << self
Expand Down
34 changes: 33 additions & 1 deletion test/test_base32_crockford.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,49 @@ def test_normalize
assert_equal "B?123", Base32::Crockford.normalize("BU-123")
end

def test_normalize_with_checksum
assert_equal "B?123", Base32::Crockford.normalize("BU-123", :checksum => true)
assert_equal "B123U", Base32::Crockford.normalize("B123U", :checksum => true)
end

def test_valid
assert_equal true, Base32::Crockford.valid?("hello-world")
assert_equal false, Base32::Crockford.valid?("BU-123")
end

def test_valid_with_checksum
assert_equal true, Base32::Crockford.valid?("B123U", :checksum => true)
assert_equal false, Base32::Crockford.valid?("BU-123", :checksum => true)
end

def test_length_and_hyphenization
assert_equal "0016J", Base32::Crockford.encode(1234, :length => 5)
assert_equal "0-01-6J",
Base32::Crockford.encode(1234, :length => 5, :split => 2)
assert_equal "00-010",
Base32::Crockford.encode(32, :length => 5, :split => 3)
end
end

def test_encoding_checksum
assert_equal "16JD",
Base32::Crockford.encode(1234, :checksum => true)
assert_equal "016JD",
Base32::Crockford.encode(1234, :length => 5, :checksum => true)
assert_equal "0-16-JD",
Base32::Crockford.encode(1234, :length => 5, :split => 2, :checksum => true)
end

def test_decoding_checksum
assert_equal 1234,
Base32::Crockford.decode("16JD", :checksum => true)
assert_equal 1234,
Base32::Crockford.decode("016JD", :length => 5, :checksum => true)
assert_equal 1234,
Base32::Crockford.decode("0-16-JD", :length => 5, :split => 2, :checksum => true)
end

def test_decoding_invalid_checksum
assert_equal nil,
Base32::Crockford.decode("16JC", :checksum => true)
end
end