lib/anybase.rb in anybase-0.0.15 vs lib/anybase.rb in anybase-0.1.0

- old
+ new

@@ -1,117 +1,132 @@ +# frozen_string_literal: true + require "anybase/version" require "securerandom" class Anybase + UnrecognizedCharacterError = Class.new(StandardError) + NegativeSignListedAsDigitError = Class.new(StandardError) + NegativeSignTooLongError = Class.new(StandardError) + UnknownNegativeSignError = Class.new(StandardError) - UnrecognizedCharacterError = Class.new(RuntimeError) + attr_reader :digits, :char_map, :num_map, :regexp, :negative_sign - attr_reader :chars, :char_map, :num_map, :regexp + def initialize(digit_string, ignore_case: false, negative_sign: nil, synonyms: {}) + raise NegativeSignTooLongError if negative_sign && negative_sign.size > 1 + raise NegativeSignListedAsDigitError if negative_sign && digit_string.index(negative_sign) - def initialize(chars, opts = nil) - @chars = chars - @ignore_case = opts && opts.key?(:ignore_case) ? opts[:ignore_case] : false - @sign = opts && opts.key?(:sign) ? opts[:sign] : nil - raise if @sign && @chars.index(@sign) - @synonyms = opts && opts[:synonyms] - @synonyms_tr = ['', ''] if @synonyms - @char_map = Hash.new{|h,k| raise UnrecognizedCharacterError.new("the character `#{k.chr}' is not included in #{@chars}")} - @num_map = {} - if ignore_case? - @chars.downcase! - end - regexp_str = '[' - @chars.split('').each_with_index do |c, i| - regexp_str << Regexp.quote(c) - add_mapping(c, i) - @num_map[i] = c - if @synonyms && @synonyms[c] - @synonyms[c].split('').each { |sc| - regexp_str << Regexp.quote(sc) - @synonyms_tr[1] << c - @synonyms_tr[0] << sc - } + self.digits = digit_string.dup + self.ignore_case = ignore_case + self.negative_sign = negative_sign + self.synonyms = synonyms + self.synonyms_tr = [String.new, String.new] + self.char_map = Hash.new { |_h,k| raise UnrecognizedCharacterError, "the character `#{k}' is not included in #{digits}" } + self.num_map = {} + + digits.downcase! if ignore_case? + + regexp_str = String.new("[") + digits.each_char.with_index do |char, i| + regexp_str << Regexp.quote(char) + char_map[char] = i + num_map[i] = char + + next unless synonyms[char] + + synonyms[char].each_char do |synonym| + regexp_str << Regexp.quote(synonym) + synonyms_tr[0] << synonym + synonyms_tr[1] << char end end - regexp_str << ']+' - @regexp = @ignore_case ? Regexp.new(regexp_str, Regexp::IGNORECASE) : Regexp.new(regexp_str) + regexp_str << "]+" + self.regexp = ignore_case? ? Regexp.new(regexp_str, Regexp::IGNORECASE) : Regexp.new(regexp_str) end def match(str) - if match = @regexp.match(str) - match.begin(0) == 0 ? match[0] : nil + if (match = regexp.match(str)) && match.begin(0).zero? + match[0] else nil end end def ignore_case? - @ignore_case + ignore_case end - def size(digits) - chars.length ** digits + def size(length) + digits.length**length end def normalize(val) - val = val.downcase if ignore_case? - @synonyms ? val.tr(*@synonyms_tr) : val + val = ignore_case? ? val.downcase : val.dup + synonyms.empty? ? val : val.tr(*synonyms_tr) end - def random(digits, opts = nil) - zero_pad = opts && opts.key?(:zero_pad) ? opts[:zero_pad] : true - number = '' - digits.times { number << chars[SecureRandom.random_number(chars.size)]} - unless zero_pad - number.sub!(/\A#{Regexp.quote(chars[0].chr)}+/, '') - number = chars[0].chr if number.empty? + def random(length, trim_leading_zeros: false) + number = String.new + length.times { number << digits[SecureRandom.random_number(digits.size)] } + + if trim_leading_zeros + number.sub!(/\A#{Regexp.quote(digits[0])}+/, "") + number = digits[0] if number.empty? end + number end def to_i(val) val = normalize(val) - op = if @sign and val[0] == @sign[0] + num = 0 + op = if negative_sign && (val[0] == negative_sign[0]) val.slice!(0, 1) :- else :+ end - num = 0 - (0...val.size).each{|i| - num = num.send(op, (chars.size ** (val.size - i - 1)) * char_map[val[i]]) - } + + (0...val.size).each do |i| + num = num.send(op, (digits.size**(val.size - i - 1)) * char_map[val[i]]) + end + num end - def to_native(val, options = nil) - if val < 0 - if @sign - val = val.abs - signed = true - else - raise - end + def to_native(val, zero_pad: 1) + zero_pad = 1 if zero_pad < 1 + negative = if val < 0 + raise UnknownNegativeSignError unless negative_sign + + val = val.abs + true + else + false end - str = '' + + str = String.new until val.zero? - digit = val % chars.size - val /= chars.size + digit = val % digits.size + val /= digits.size str[0, 0] = num_map[digit] end - if options && options[:zero_pad] && str.size < options[:zero_pad] - str[0, 0] = num_map[0] * (options[:zero_pad] - str.size) - end - str == '' ? num_map[0].dup : (signed ? @sign.dup << str : str) - end - def add_mapping(c, i) - char_map[c[0]] = i + str[0, 0] = num_map[0] * (zero_pad - str.size) if str.size < zero_pad + str[0, 0] = negative_sign if negative + + str end - private :add_mapping - Hex = Anybase.new('0123456789abcdef', :ignore_case => true) - Base62 = Anybase.new('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') - Base64 = Anybase.new('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') - Base64ForURL = Anybase.new('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_') - Base73ForURL = Anybase.new('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$-_.+!*\'(),') + private + + attr_writer :digits, :char_map, :num_map, :regexp, :negative_sign + attr_accessor :ignore_case, :synonyms, :synonyms_tr +end + +class Anybase + Hex = Anybase.new("0123456789abcdef", ignore_case: true) + Base62 = Anybase.new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + Base64 = Anybase.new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + Base64ForURL = Anybase.new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") + Base73ForURL = Anybase.new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$-_.+!*'(),") end