def initialize string case string when String @string, @ptree = string, nil when Node @string, @ptree = nil, string else @string, @ptree = String(string), nil end @copy = @lexstat = nil end # # === LEXICAL ANALYZER === # def rewind @copy = @string.dup.strip @lexstat = nil end RE_SPACE = '([ \t])' RE_INTEGER = '([-+]?\d+)' RE_EXP = '([eE][-+]?[0-9]+)' RE_REAL = "([-+]?[0-9]*(\\.[0-9]*#{RE_EXP}?|#{RE_EXP}))" RE_YEAR = "([-+]?[0-9]{1,4})" RE_MONTH = "(0?[1-9]|1[0-2])" RE_DAY = "([12][0-9]|30|31|0?[1-9])" RE_HOUR = "(2[0-3]|[0-1]?[0-9])" RE_MINUTE = "([0-5]?[0-9])" RE_SECOND = "((#{RE_MINUTE}|60)(\\.[0-9]*)?)" RE_NAME = "(%|[a-zA-Z][a-zA-Z_]*([0-9]+[a-zA-Z_]+)*)" RE_DATE = "#{RE_YEAR}-#{RE_MONTH}-#{RE_DAY}" RE_TIME = "#{RE_HOUR}((:[0-5]?[0-9]|[0-5][0-9])(:#{RE_SECOND})?)?" RE_HandM = "#{RE_HOUR}((:[0-5]?[0-9]|[0-5][0-9]))?" def next_token # decomment @copy.sub!(/^#.*/, ''); if @copy.sub!(%r{^\s*(\))}, '') then @lexstat = nil return [$1, $1] end if @copy.sub!(%r{^\s*(\()\s*}, '') then return [$1, $1] end if @copy.sub!(%r{^[ \t]*(@|after|from|since|ref)[ \t]*}i, '') then @lexstat = :SHIFT_SEEN return [:SHIFT, $1] end if @copy.sub!(%r{^[ \t]*(per|PER|/)[ \t]*}, '') then @lexstat = nil return [:DIVIDE, $1] end if @copy.sub!(%r{^(\^|\*\*)}, '') then @lexstat = nil return [:EXPONENT, $1] end if @copy.sub!(%r{^(\.|\*|[ \t]+)}, '') then @lexstat = nil return [:MULTIPLY, $1] end if :SHIFT_SEEN === @lexstat \ and @copy.sub!(%r{^#{RE_DATE}T?[ \t]*}, '') then y, m, d = $1, $2, $3 @lexstat = :DATE_SEEN return [:DATE, XDate.new(y.to_i, m.to_i, d.to_i)] end if :SHIFT_SEEN === @lexstat \ and @copy.sub!(%r{^now[ \t]*}, '') then @lexstat = nil return [:DATE, :now] end if :DATE_SEEN === @lexstat \ and @copy.sub!(%r{^#{RE_TIME}[ \t]*}, '') then h, m, s = $1, $3, $5 m = m.sub(/:/,'') if m s = 0 if s.nil? @lexstat = :TIME_SEEN return [:TIME, ((h.to_i * 60 + m.to_i) * 60 + Float(s))] end if :DATE_SEEN === @lexstat \ and @copy.sub!(%r{^([0-2][0-9])([0-5][0-9])[ \t]*}, '') then h, m = $1, $2 @lexstat = :TIME_SEEN return [:TIME, ((h.to_i * 60 + m.to_i) * 60.0)] end if :DATE_SEEN === @lexstat \ and @copy.sub!(%r{^([0-9])([0-5][0-9])[ \t]*}, '') then h, m = $1, $2 @lexstat = :TIME_SEEN return [:TIME, ((h.to_i * 60 + m.to_i) * 60.0)] end if :TIME_SEEN === @lexstat \ and @copy.sub!(%r{^UTC[ \t]*}, '') then @lexstat = nil return [:ZONE, 0] end if :TIME_SEEN === @lexstat \ and @copy.sub!(%r{^([-+]?)#{RE_HandM}[ \t]*}, '') then sgn, h, m = $1, $2, $4 m = m.sub(/:/,'') if m @lexstat = nil h = h.to_i h = -h if sgn == "-" m = m.to_i m = -m if sgn == "-" return [:ZONE, ((h * 60) + m)] end if @copy.sub!(%r{^#{RE_NAME}}, '') then @lexstat = nil return [:NAME, $1] end if @copy.sub!(%r{^#{RE_REAL}}, '') then @lexstat = nil return [:REAL, $1.to_f] end if @copy.sub!(%r{^#{RE_INTEGER}}, '') then @lexstat = nil return [:INT, $1.to_i] end if @copy.sub!(%r{^(-)}, '') then @lexstat = nil return [:MULTIPLY, $1] end if @copy.sub!(%r{^(.)}, '') then return [$1, $1] end return [false, false] end # # === USER LEVEL METHODS === # def tokens rewind x = [] while (t = next_token).first x.push t end x end def do_parse2 rewind return ErrorNode.new('') if @string.nil? or @string.empty? pa = do_parse pa ? pa : ErrorNode.new(@string) end def ptree @ptree = do_parse2 if not @ptree @ptree end def dup @string ? self.class.new(@string) : self.class.new(@ptree) end def parse dup.parse! end def parse! @ptree = do_parse2 if not @ptree self end def self::parse(string) new(string).parse! end =begin --- reduce0 just do nothing. =end def reduce0 self end =begin --- reduce1 removes unnecessary parentheses. =end def reduce1 @string = ptree.to_s self end =begin --- reduce2 removes shift operator within multiplication/division/exponent =end def reduce2 @ptree = ptree.reduce2 @string = nil self end =begin --- reduce3 flattens expression and collects all factors =end def reduce3 @ptree = ptree.reduce3 @string = nil self end =begin --- reduce4 collects terms with the same name =end def reduce4 @ptree = ptree.reduce4 @string = nil self end =begin --- reduce5 expands all terms recursively =end def reduce5 @ptree = ptree.reduce5 @string = nil self end attr_reader :string def to_s @string = @ptree.to_s if @string.nil? @string end def inspect if @ptree.nil? then "Units{#{@string}}" else "Units[#{@ptree.inspect}]".gsub(/Units::/, '').gsub(/Node\[/, '[') end end def self::[](string) new(string) end def self::parse(string) new(string).parse! end def eval(x = 0) r5 = ptree.reduce5 case r = r5.ref when TimeNode r.add(x, r5.name) else fac = NumberNode.new(x + r.value) self.class.new(MulNode.new(fac, r5.deref)) end end def convert(numeric, to_units) to_units = Units.new( to_units ) if to_units.is_a?(String) r5 = dup.ptree.reduce5 case r = r5.ref when TimeNode r.add_time(r5.deref.mul(numeric)).div_time(to_units.ptree) else shift1 = r.value numeric = shift1 + numeric if shift1 != 0 fact = r5.divide(tp = to_units.dup.ptree).reduce5.value numeric *= fact if fact != 1 shift2 = tp.reduce5.ref.value numeric = numeric - shift2 if shift2 != 0 numeric end end def factor_and_offset(to_units) # To convert a numeric from self to to_units: # scale_factor * numeric + add_offset to_units = Units.new( to_units ) if to_units.is_a?(String) add_offset = convert(0, to_units) scale_factor = convert(1, to_units) - add_offset [ scale_factor, add_offset ] end def convert2(val, to_units) # Like Units#convert, but applicable to any Numeric-like objects. # Returns the original value if the units are incompatible. to_units = Units.new( to_units ) if to_units.is_a?(String) if ( self == to_units ) val elsif ( self =~ to_units ) if Numeric===val convert( val, to_units ) else factor, offset = factor_and_offset( to_units ) val*factor + offset end else unless $VERBOSE.nil? $stderr.print( "*WARNING*: " + "incompatible units: #{self.to_s} and #{to_units.to_s}\n") caller(0).each{|c| $stderr.print "\t* ",c,"\n"} end val end end @@reduce = :reduce4 def self::reduce_level @@reduce.to_s[-1] end def self::reduce_level=(n) @@reduce = case n when 1 then :reduce1 when 2 then :reduce2 when 3 then :reduce3 when 4 then :reduce4 else :reduce5 end end def binop(op, other) case other when Numeric other = NumberNode.new(other) when Units other = other.ptree end q = self.ptree.send(op, other).send(@@reduce) Units.new(q) end def *(other) binop(:mul, other) end def **(other) binop(:pow, other) end def /(other) binop(:divide, other) end def ^(other) binop(:shift, other) end def ==(other) case other when self.class dup.reduce5.to_s == other.dup.reduce5.to_s else false end end #def === (other) # reduce5.ptree.deref.to_s == other.reduce5.ptree.deref.to_s #end alias === == #def === (other) # # returns true if other is within a factor and/or offset of difference. # case other # when self.class # (self/other).reduce5.ptree.children.each do |child| # return false if !( NumberNode === child ) # end # true # else # false # end #end def =~(other) case other when self.class (self/other).reduce5.ptree.children.each{ |node| return false unless NumberNode === node } true else false end end def self::pow_f(a, b) if Integer === b and b < 0 then a ** b.to_f else a ** b end end