Class: IdCoder

Inherits:
Object
  • Object
show all
Includes:
ModalSupport::BracketConstructor
Defined in:
lib/id_coder.rb

Overview

Id-codes are relatively short codes to represent externally id internal numbers.

A bijection between ids and codes is used to assure uniqueness of codes and to avoid storing the codes in the database (codes and ids are computed on-the-fly from each other).

Codes are user-visible; they’re designed to be the user’s identification of some element.

Only digits an capital letters are used for the codes, excluding I,O,1,0 to avoid confusion, and a check digit can be used to detect most common transcription errors. This makes codes viable to be transmitted orally, etc.

The number of digits used is scalable: the minimum number of digits and the increment-size can be parameterized to render good-looking codes.

An IdCoder can be defined passing to it three optional parameters that define the lenght of the codes:

IdCoder[:num_digits=>4, :block_digits=>3, :check_digit=>false]

An additional parameter can be passed to define which characters will be used as digits for the codes and its order; this must be a String of IdCoder::RADIX distinct characters:

IdCoder[:code_digits=>"0123456789ABCDEFGHIJKLMNOPQRSTUV"]

Alternatively, a seed parameter can be passed to generate a randomized string

IdCoder[:seed=>8734112]

Since this might produce different results in different Ruby versions, the code_digits produced can be accessed and kept for portability:

IdCoder[:seed=>8734112].code_digits # -> "9GTFNJSZA6LQWXV3BEKHYMP75U8R24CD"

Defined Under Namespace

Classes: InvalidCode, InvalidCodeDigits, InvalidNumber

Constant Summary collapse

RADIX =

Internal parameters

32
DIGITS =

RADIX-digits used by Integer#to_s(RADIX), String#to_i(RADIX)

"0123456789abcdefghijklmnopqrstuv"
CODE_DIGITS =

RADIX-digits used for the codes (permutation of 2-9,A-Z except I,O)

"BAFTQ4EJCNVYZKDPG37H5S8WML692RXU"

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ IdCoder

Returns a new instance of IdCoder.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/id_coder.rb', line 41

def initialize(config={})
  config = Settings[config]
  if config.code_digits
    @code_digits = config.code_digits
    raise InvalidCodeDigits, "Invalid digits" if @code_digits.bytes.to_a.uniq.size!=RADIX
  else
    @code_digits = CODE_DIGITS
    if config.seed
      srand config.seed
      @code_digits = @code_digits.bytes.to_a.shuffle.map(&:chr)*""
    end
  end
  @num_digits = (config.num_digits || 6).to_i
  @block_digits = (config.block_digits || 2).to_i
  @check_digit = config.check_digit
  @check_digit = true if @check_digit.nil?
end

Instance Method Details

#block_digitsObject

Number of digits for incremental scaling-up blocks



76
77
78
# File 'lib/id_coder.rb', line 76

def block_digits
  @block_digits
end

#check_digit?Boolean

Use a check digit?: this increases the length of codes in one

Returns:

  • (Boolean)


71
72
73
# File 'lib/id_coder.rb', line 71

def check_digit?
  @check_digit
end

#code_digitsObject



113
114
115
# File 'lib/id_coder.rb', line 113

def code_digits
  @code_digits
end

#code_lengthObject

mininum code_length



83
84
85
# File 'lib/id_coder.rb', line 83

def code_length
  num_digits + (check_digit? ? 1 : 0)
end

#code_length_for(id) ⇒ Object



109
110
111
# File 'lib/id_coder.rb', line 109

def code_length_for(id)
  num_digits_for(id) + (check_digit? ? 1 : 0)
end

#code_to_id(code) ⇒ Object

Inverse encoding: compute the integer id for a Id-Code

Raises:



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/id_coder.rb', line 147

def code_to_id(code)
  raise InvalidCode, "Codes must be strings" unless code.kind_of?(String)
  code = code.strip.upcase # tolerate case differences & surrounding whitespace
  raise InvalidCode, "Invalid code: #{code}" unless code =~ /\A[#{@code_digits}]+\Z/
  # raise InvalidCode, "Invalid code length: #{code.size} for #{code} (must be #{code_length})" unless code.size==code_length
  if check_digit?
    cd = code[-1,1]
    code = code[0...-1]
    raise InvalidCode, "Invalid code: #{code+cd}" if cd!=check_digit(code)
  end
  nd = code.size
  sx = nd-num_digits
  raise InvalidCode, "Invalid code length: #{code.size} for #{code}" unless sx>=0 && (sx%block_digits)==0
  i = 0
  v = ""
  mask = 0
  code.each_byte do |b|
    next_mask = @code_digits.index(b.chr)
    d  = ((next_mask ^ mask)-i)%RADIX
    mask = next_mask
    d = DIGITS[d,1]
    v = d + v
    i += 1
  end
  v.to_i(RADIX)
end

#id_to_code(id) ⇒ Object

Direct encoding: generate an Id-Code from an integer id.

Raises:



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/id_coder.rb', line 128

def id_to_code(id)
  raise InvalidNumber, "Numbers to be coded must be passed as integers" unless id.kind_of?(Integer)
  raise InvalidNumber, "Negative numbers cannot be encoded: #{id}" if id<0
  nd = num_digits_for(id)
  v = id.to_s(RADIX)
  v = "0"*(nd-v.size) + v
  i = 0
  code = ""
  mask = 0
  v.reverse.each_byte do |b|
    d = DIGITS.index(b.chr)
    code << @code_digits[mask = ((d+i)%RADIX ^ mask),1]
    i += 1
  end
  code << check_digit(code) if check_digit?
  code
end

#inspectObject



121
122
123
# File 'lib/id_coder.rb', line 121

def inspect
  "IdCoder[#{parameters.inspect.unwrap('[]')}]"
end

#num_digitsObject

Minumum number of digits used; limits the number of valid ids before scaling up to RADIX**num_digits



66
67
68
# File 'lib/id_coder.rb', line 66

def num_digits
  @num_digits
end

#num_digits_for(id) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/id_coder.rb', line 92

def num_digits_for(id)
  if id<num_valid_codes
    num_digits
  else
    # ((Math.log(id)/Math.log(RADIX)-code_length)/block_digits).ceil*block_digits + num_digits
    nd = num_digits
    loop do
      nd += block_digits
      nvc = RADIX**nd
      break if id<nvc
    end
    # check = ((Math.log(id)/Math.log(RADIX)-code_length)/block_digits).ceil*block_digits + num_digits
    # raise "nd=#{nd}; [#{check}] id=#{id}" unless nd==check
    nd
  end
end

#num_valid_codesObject

Number of valid codes before scaling-up



88
89
90
# File 'lib/id_coder.rb', line 88

def num_valid_codes
  RADIX**num_digits
end

#parametersObject



117
118
119
# File 'lib/id_coder.rb', line 117

def parameters
  { :num_digits=>@num_digits, :block_digits=>@block_digits, :check_digit=>@check_digit, :code_digits=>@code_digits }
end

#valid_code?(code) ⇒ Boolean

Check code: returns true (valid) or false (invalid)

Returns:

  • (Boolean)


175
176
177
178
179
180
181
182
183
# File 'lib/id_coder.rb', line 175

def valid_code?(code)
  is_valid = true
  begin
    code_to_id(code)
  rescue InvalidCode
    is_valid = false
  end
  is_valid
end