lib/unit/system.rb

Summary

Maintainability
C
7 hrs
Test Coverage
# -*- coding: utf-8 -*-
require 'yaml'

class Unit < Numeric
  class System
    attr_reader :name, :unit, :unit_symbol, :factor, :factor_symbol, :factor_value

    def initialize(name)
      @name = name
      @unit = {}
      @unit_symbol = {}

      # one is internal trivial factor
      @factor = {one: {symbol: 'one', value: 1} }
      @factor_symbol = {'one' => :one}
      @factor_value = {1 => :one}

      @loaded_systems = []
      @loaded_filenames = []

      yield(self) if block_given?
    end

    def load(input)
      case input
      when Hash
        data = input
      when IO
        data = YAML.load(input.read)
      when String
        if File.exist?(input)
          return if @loaded_filenames.include?(input)
          data = YAML.load_file(input)
          @loaded_filenames << input
        else
          load(input.to_sym)
          return
        end
      when Symbol
        return if @loaded_systems.include?(input)
        data = YAML.load_file(File.join(File.dirname(__FILE__), 'systems', "#{input}.yml"))
        @loaded_systems << input
      end

      load_factors(data['factors']) if data['factors']
      load_units(data['units']) if data['units']

      @unit.each {|name, unit| validate_unit(unit[:def]) }

      true
    end

    def validate_unit(units)
      units.each do |factor, unit, exp|
        #raise TypeError, 'Factor must be symbol' if !(Symbol === factor)
        #raise TypeError, 'Unit must be symbol' if !(Numeric === unit || Symbol === unit)
        #raise TypeError, 'Exponent must be numeric' if !(Numeric === exp)
        raise TypeError, "Undefined factor #{factor}" if !@factor[factor]
        raise TypeError, "Undefined unit #{unit}" if !(Numeric === unit || @unit[unit])
      end
    end

    def parse_unit(expr)
      stack, result, implicit_mul = [], [], false
      expr.to_s.scan(TOKENIZER).each do |tok|
        if tok == '('
          stack << '('
          implicit_mul = false
        elsif tok == ')'
          compute(result, stack.pop) while !stack.empty? && stack.last != '('
          raise(SyntaxError, 'Unexpected token )') if stack.empty?
          stack.pop
          implicit_mul = true
        elsif OPERATOR.key?(tok)
          compute(result, stack.pop) while !stack.empty? && stack.last != '(' && OPERATOR[stack.last][1] >= OPERATOR[tok][1]
          stack << OPERATOR[tok][0]
          implicit_mul = false
        else
          val = case tok
                when REAL   then [[:one, tok.to_f, 1]]
                when DEC    then [[:one, tok.to_i, 1]]
                when SYMBOL then symbol_to_unit(tok)
                end
          stack << '*' if implicit_mul
          implicit_mul = true
          result << val
        end
      end
      compute(result, stack.pop) while !stack.empty?
      result.last
    end

    private

    REAL   = /^-?(?:(?:\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?|\d+[eE][-+]?\d+)$/
    DEC    = /^-?\d+$/
    SYMBOL = /^[a-zA-Z_°'"][\w°'"]*$/
    OPERATOR = { '/' => ['/', 1], '*' => ['*', 1], '·' => ['*', 1], '^' => ['^', 2], '**' => ['^', 2] }
    OPERATOR_TOKENS = OPERATOR.keys.sort_by {|x| -x.size }. map {|x| Regexp.quote(x) }
    VALUE_TOKENS = [REAL.source[1..-2], DEC.source[1..-2], SYMBOL.source[1..-2]]
    TOKENIZER = Regexp.new((OPERATOR_TOKENS + VALUE_TOKENS + ['\\(', '\\)']).join('|'))

    def lookup_symbol(symbol)
      if unit_symbol[symbol]
        [[:one, unit_symbol[symbol], 1]]
      else
        found = factor_symbol.keys.find do |sym|
          symbol[0..sym.size-1] == sym && unit_symbol[symbol[sym.size..-1]]
        end
        [[factor_symbol[found], unit_symbol[symbol[found.size..-1]], 1]] if found
      end
    end

    def symbol_to_unit(symbol)
      lookup_symbol(symbol) ||
        (symbol[-1..-1] == 's' ? lookup_symbol(symbol[0..-2]) : nil) || # Try english plural
        [[:one, symbol.to_sym, 1]]
    end

    def compute(result, op)
      b = result.pop
      a = result.pop || raise(SyntaxError, "Unexpected token #{op}")
      result << case op
                when '*' then a + b
                when '/' then a + Unit.power_unit(b, -1)
                when '^' then Unit.power_unit(a, b[0][1])
                else raise SyntaxError, "Unexpected token #{op}"
                end
    end

    def load_factors(factors)
      factors.each do |name, factor|
        name = name.to_sym
        symbols = [factor['sym'] || []].flatten
        base, exp = factor["def"].to_s.split("^").map { |value| Integer(value) }
        exp ||= 1
        raise "Invalid definition for factor #{name}" unless base
        value = base ** exp
        $stderr.puts "Factor #{name} already defined" if @factor[name]
        @factor[name] = { symbol: symbols.first, value: value }
        symbols.each do |sym|
          $stderr.puts "Factor symbol #{sym} for #{name} already defined" if @factor_symbol[name]
          @factor_symbol[sym] = name
        end
        @factor_symbol[name.to_s] = @factor_value[value] = name
      end
    end

    def load_units(units)
      units.each do |name, unit|
        name = name.to_sym
        symbols = [unit['sym'] || []].flatten
        $stderr.puts "Unit #{name} already defined" if @unit[name]
        @unit[name] = { symbol: symbols.first, def: parse_unit(unit['def'])  }
        symbols.each do |sym|
          $stderr.puts "Unit symbol #{sym} for #{name} already defined" if @unit_symbol[name]
          @unit_symbol[sym] = name
        end
        @unit_symbol[name.to_s] = name
      end
    end

    SI = new('SI') do |system|
      system.load(:si)
      system.load(:binary)
      system.load(:degree)
      system.load(:time)
    end

    Unit.default_system = SI
  end
end