Never been to DZone Snippets before?

Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

About this user

Sam McCall

« Newer Snippets
Older Snippets »
Showing 1-4 of 4 total  RSS 

Open an arbitrary number of resources safely in ruby

I'm too lazy to work out what happens if I try
filenames.map {|f| File.open(f) }
and the thirteenth file doesnt exist, but I bet I don't like it.

module Enumerable
  # Example:
  # ['a','b'].with_files {|f,g| ... }
  # is the same as
  # File.open('a') {|f| File.open('b') {|g| ... } }
  # You can specify modes with
  # [['a', 'rb'], ['b', 'w']].with_files ...
  def with_files(
      meth = File.method(:open),
      offset=0,
      inplace=false,
      &block
  )
    if inplace then
      if offset >= length then
        yield self
      else
        fname,mode = *self[offset]
        File.open(fname,mode) {|f| 
          self[offset] = f
          self.with_files(meth,offset+1,true,&block)
        }
      end
    else
      dup.with_files(meth,offset,true,&block)
    end
  end
end

procalyzer - analyze proc frequency from WoW combat log

# Usage:
# File.open('Logs/WowCombatLog.txt') {|log| 
#    Procalyzer::procalyze(log,"Light's Ward",&Procalyzer::PLAYER_SWING_OR_SPELL).dump
# }

require 'time'
require 'csv'

Stats = Struct.new(:min,:max,:n,:sx,:sx2)

# calculate mean, stdev, etc for an array of numbers
class Stats 
  def initialize(population)
    self.n = 0
    self.sx = self.sx2 = 0.0
    population.each { |x|
      self.n += 1
      self.sx += x
      self.sx2 += x*x;
      self.min = x if self.min.nil? or x < self.min
      self.max = x if self.max.nil? or x > self.max
    }
  end

  def mean
    sx/n
  end
  
  def variance
    sx2/n - (sx/n)**2
  end
 
  def stdev
    variance ** 0.5
  end

  def to_s
    "#{mean} (dev: #{stdev}, range: #{min} - #{max}, n: #{n})"
  end
end

module Procalyzer
	# wow 2.4 log entry
	LogEntry = Struct.new(:time,:message,:actor_guid,:actor_name,:actor_id,:target_guid,:target_name,:target_id,:rest)

	# parse a log into a list of LogEntries.
	# if block given, yields each entry and returns a list of the result
	# else resturns a list of entries
	def self.parse_log(stream) 
		lines = []
		while line=stream.gets
			entry = LogEntry.new
			line =~ %r{^(\d+)/(\d+) (\d\d):(\d\d):(\d\d).(\d\d\d) (.*)$} or raise "Couldnt parse line: #{line}"
			entry.time = Time.local(Time.new.year, $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, 1000*$6.to_i)
			entry.message, 
				entry.actor_guid, entry.actor_name, entry.actor_id, 
				entry.target_guid, entry.target_name, entry.target_id, 
				*rest = *CSV.parse_line($7.strip)
			entry.rest = rest
			if block_given?
				lines << yield(entry)
			else
				lines << entry
			end
		end
		lines
	end

	# Scans a logfile, runs event handlers for events matching certain filters
	class EventScanner
		def initialize
			@filters = []
			@start = []
			@end = []
		end
	
		# add a handler to be run for events matching filter
		def on(filter, &block)
			@filters << [filter,block]
		end
		# add a handler at the start
		def start(&block)
			@start << block
		end
		# add a handler at the end
		def end(&block)
			@end << block
		end
	
		# scan the stream and run handlers
		def run(file) 
			@start.each {|s| s.call() }
			Procalyzer::parse_log(file) {|entry|
				@filters.each {|filter,block|
					block.call(entry) if filter[entry]
				}
			}
			@end.each {|s| s.call() }
		end
	end

	# Container for analysis results
	# durations is a list of the proc lengths found
	# intervals is the list of times from one proc to the next
	# cooldown is the estimated or specified internal cooldown
	# procs is a list of 1 or 0 entries specifying whether each eligible event procced the aura
	ProcResults = Struct.new(:durations, :intervals, :cooldown, :procs)
	class ProcResults
		def initialize
			self.durations=[]
			self.intervals=[]
			self.procs=[]
		end
		# print results in a pretty human readable form
		def dump(out=$stdout)
			if self.durations.empty?
				out.puts "Proc did not occur."
			elsif self.intervals.empty?
				out.puts "Proc occurred once only."
			else
				dur_stats = Stats.new(durations)
				out.puts "Duration: #{dur_stats}"
				if dur_stats.stdev/dur_stats.mean > 0.1
					out.puts "Standard deviation of duration is more than 10%."
					out.puts "This can indicate a proc with no cooldown that refreshes itself."
				end
				interval_stats = Stats.new(intervals)
				out.puts "Interval: #{interval_stats}"
				out.printf("Uptime: %.2f%%\n", 100.0*(dur_stats.sx - durations[-1])/interval_stats.sx)
				out.puts "Internal cooldown: #{cooldown}"
				proc_stats = Stats.new(procs)			
				out.printf("Proc chance: %.2f%% (%d/%d)\n", 100*proc_stats.mean, proc_stats.sx.to_i, proc_stats.n)
			end
		end
	end

	# Analyse a log file, looking for a self-buff triggered by an event.
	# Assume it has a fixed % to proc unless it is on cooldown (or already active, 
	# in which case it could theoretically proc but won't slow in the log)
	# we do this in two phases:
	# 1) measure proc durations and intervals, and estimate cooldown if it's not specified
	#    (just minimum interval rounded down to nearest second)
	# 2) count eligible proccing events (ones outside cooldown) and see which procced it
	# file is a seekable stream containing the log
	# auraname is the buff name like "Haste"
	# cooldown is the internal cooldown if known (else will be estimated)
	# proc_trigger is a filter block for proc events (takes an event, returns true if the event can proc
	#   the aura, assuming the aura isnt on cooldown or already up)
	# returns a procresults object
	def self.procalyze(file, auraname, cooldown=nil, &proc_trigger) 
		results = ProcResults.new
		results.cooldown=cooldown

		scanner = EventScanner.new
	
		start = nil
		last = nil
	
		# Aura start: record start time and interval
		scanner.on(proc {|e| e.message == "SPELL_AURA_APPLIED" and e.rest[1] == auraname} ) {|evt|
			start = evt.time.to_f
			results.intervals << (start-last) if last
			last = start
		}
		# Aura finish: record duration
		scanner.on(proc {|e| e.message == "SPELL_AURA_REMOVED" and e.rest[1] == auraname} ) {|evt|
			if start
				duration = evt.time.to_f - start
				results.durations << duration
			end
		}
		scanner.run(file)

		return results if results.intervals.length==0

		# Estimate cooldown if not given	
		if results.cooldown.nil?
			interval_stats = Stats.new(results.intervals)
			duration_stats = Stats.new(results.durations)
			if(duration_stats.stdev/duration_stats.mean > 0.1)
				results.cooldown = 0.0
			else
				results.cooldown = interval_stats.min.floor
			end
			puts "Using cooldown = #{results.cooldown}" if $DEBUG
		end
	
		file.pos = 0 # restart file
		last_swing_time = nil
		last_swing_could_proc = false
		aura_up = false
		start = nil
		scanner = EventScanner.new

		start_swing = proc{|evt| last_swing_time = evt.time.to_f; last_swing_count_proc = true  }
		end_swing = proc{
			if(last_swing_could_proc)
				puts "Possible proc: #{last_swing_time}" if $DEBUG
				results.procs << (aura_up ? 1 : 0)
			end
			last_swing_could_proc = false
			last_swing_time = nil
		}

		# Aura start, start cooldown timer and set aura_up
		scanner.on(proc {|e| e.message == "SPELL_AURA_APPLIED" and e.rest[1] == auraname} ) {|evt|
			puts "Aura start: #{evt.time.to_f}" if $DEBUG
			start = evt.time.to_f
			aura_up = true
			end_swing.call()
		}
		# Aura end, clear aura_up
		scanner.on(proc {|e| e.message == "SPELL_AURA_REMOVED" and e.rest[1] == auraname} ) {|evt|
			aura_up = false
		}
		# Proc trigger event - eg a melee swing
		scanner.on(proc_trigger) {|evt|
			# first, this is the point where we confirm the previous swing didnt proc the aura
			end_swing.call()

			# record details of this swing, incl whether it could proc
			time_since_last = (start.nil?) ? 99999.0 : (evt.time.to_f - start)
			last_swing_could_proc = (not aura_up) && (time_since_last > results.cooldown)
			last_swing_time = evt.time.to_f
		}
		# at the end of the log file, confirm that the last swing didnt proc the aura
		scanner.end(&end_swing) 

		scanner.run(file)
		results
	end

	# actor_id for the player
	PLAYER_ID = "0x511"

	# predefined filters for common procs
	PLAYER_SWING = proc{|evt| evt.actor_id == PLAYER_ID and evt.message="SWING_DAMAGE" }
	PLAYER_SWING_OR_SPELL = proc{|evt| evt.actor_id == PLAYER_ID and (evt.message=="SWING_DAMAGE" or evt.message=="SPELL_DAMAGE") }
	PLAYER_SPELL_DAMAGE = proc{|evt| evt.actor_id == PLAYER_ID and evt.message=="SPELL_DAMAGE" }
        PLAYER_RANGED_OR_SPELL = proc{|evt| evt.actor_id == PLAYER_ID and (evt.message=="RANGE_DAMAGE" or evt.message=="SPELL_DAMAGE") }
end

Ace2 mod manager

#!/usr/local/bin/ruby

require 'open-uri'
require 'fileutils'

# should be placed inside WoW directory
# usage:
# ace_updater                  - updates all installed mods
# ace_updater update           - updates all installed mods
# ace_updater update Omen Grid - updates selected mods
# ace_updater add Threat-1.0   - install a new mod
# ace_updater delete FuBar     - uninstall an existing mod
# add the -v flag to an update to show changes from the changelog.

# You may need to adjust zip_extract for your system.
# file is the zip file to be extracted
# target_dir is the existing directory to extract into.

class Updater
	# requirers some OS customisation, this WFM on Mac OS X
	def zip_extract(file,target_dir)
		system(
			"unzip", "-q", file, "-d", target_dir
		) or raise "Error extracting (#{$?}): unzip #{file.inspect} -d #{target_dir.inspect}"
	end	

	def initialize(wow_path,opts,listing_url="http://files.wowace.com/")
		@opts=opts
		@wow_path = wow_path
		@addons_dir = File.join(@wow_path,"Interface","Addons")
		@addons_download_dir = File.join(@wow_path,"Interface","ace_updater")
		@listing_url=listing_url
	end

	def mod_index
		@mod_index ||= begin
			puts "Fetching Ace mod index from #{@listing_url}..."
			m={}
			open(@listing_url) {|f| 
				f.read 
			}.scan(
				%r{<td>\s*<a href="([^"]+)">([^>]+)</a>\s*</td>\s*<td>r(\d+)</td>}
			) {|url,mod,revision|
				m[mod] = [revision.to_i,url]
			} 
			puts "#{m.length} entries found."
			puts
			m
		end
	end

	def each_installed_mod
		Dir.foreach(@addons_dir) {|addon|
			next if ["..","."].include? addon 
			yield addon,installed_revision(addon)
		}
	end

	def installed_revision(addon)
		addon_dir = File.join(@addons_dir,addon)
		x = Dir.glob(File.join(addon_dir,"Changelog-#{addon}-r*.xml"))
		return nil if x.empty?
		x[0] =~ /Changelog-#{addon}-r(\d+)\.xml/
		$1.to_i
	end

	def get_changes(changelog,local_revision) 
		changes=[]
		current_rev = "?????"
		reading_changes = false
		File.open(changelog) {|f|
			f.read
		}.each_line{|l|
			l.strip!
			if l =~ /^r(\d+) \|/
				current_rev = $1.to_i
				break if current_rev <= local_revision
			elsif l=~/^-+$/
				reading_changes = false
			elsif l=~/^$/
				reading_changes = true
			else
				changes << [current_rev,l] if reading_changes
			end
		}
		changes.stable_sort_by {|r,t| r}
	end

	def update_mods(list=nil)
		list=nil if list.empty?

		remote_mods = mod_index()

		puts "Scanning #{list ? 'chosen' : 'all'} mods for updates..."
		jobs = []
		each_installed_mod {|mod,revision|
			next if revision.nil? #non-ace
			next if list and not list.include?(mod) #not on the list

			remote_revision,url = remote_mods[mod]
			if remote_revision.nil? or remote_revision < revision
				nonfatal("#{mod}: local is newer than remote (r#{revision.inspect} vs r#{remote_revision.inspect}). Skipping.")
			elsif remote_revision > revision
				jobs << [mod,revision,remote_revision,url]
			end

			list.delete mod if list
		}
		list.each {|x|
			$stderr.puts "WARNING: #{x} is not an installed Ace addon, skipping update."
		} if list

		if jobs.length == 0
			puts "#{list ? 'Chosen' : 'All'} mods up to date."
		else
			install_mods(jobs)
		end
	end

	def add_mods(modlist)
		remote_mods = mod_index()
		jobs = modlist.map {|mod|
			if remote_mods.key?(mod)
				rev,url = remote_mods[mod]
				[mod,nil,rev,url]
			else
				$stderr.puts "WARNING: #{mod} not found in mod index, skipping"
			end
		}.compact
		install_mods(jobs)
	end

	def delete_mods(modlist)
		modlist = modlist.map {|x|
			next if ['.','..'].include?(x) # this would be lame
			rev = installed_revision(x)
			if rev
				[x,rev]
			else
				nonfatal("#{x} is not an installed Ace addon, skipping")
			end
		}.compact

		modlist.each_with_index {|(mod,rev),i|
			status(i,modlist.length,"delete",mod,rev)
			FileUtils.remove_dir(File.join(@addons_dir,mod))
		}
	end
	
	def install_mods(jobs)
		FileUtils.remove_dir(@addons_download_dir) if File.directory?(@addons_download_dir)
		Dir.mkdir(@addons_download_dir) 
		jobs.each_with_index{|(mod,revision,remote_revision,url),index|
			status(index, jobs.length, (revision ? 'update' : 'add'), mod, *([revision,remote_revision].compact))

			zipfile = File.join(@addons_download_dir,"#{mod}.zip")

			File.open(zipfile,'w') {|f|
				open(url) {|data|
					FileUtils.copy_stream(data,f)
				}
			}

			zip_extract(zipfile,@addons_download_dir)
			expected_output_dir = File.join(@addons_download_dir,mod)
			destination_dir = File.join(@addons_dir,mod)
			backup_dir = "#{destination_dir}.backup"

			unless File.directory?(expected_output_dir)
				nonfatal("#{expected_output_dir} not created as expected")
				nonfatal("Check #{zipfile}")
				nonfatal("Skipping installation of #{mod}")
			end

			changelog = File.join(expected_output_dir,"Changelog-#{mod}-r#{remote_revision}.xml")
			# for some reason these often get wrong privs
			FileUtils.chmod 0644,changelog
			if revision and @opts.include?('-v') # show changelog
				changes = get_changes(changelog,revision)
				changes.each {|revision,text|
					puts("   * [r%d] %s" % [revision,text])
				}
			end

			FileUtils.remove(zipfile)
			FileUtils.mv(destination_dir,backup_dir) if revision
			FileUtils.mv(expected_output_dir,destination_dir)
			FileUtils.remove_dir(backup_dir) if revision
		}
		FileUtils.remove_dir(@addons_download_dir)
	end

	def status(sequence,total,action,mod,rev1,rev2=nil)
		rev2 = " -> r#{rev2}" if rev2
		puts("[%2d/%2d] %6s %16s r%d%s" % [sequence+1,total,action,mod,rev1,rev2])
	end

	def nonfatal(s)
		$stderr.puts("WARNING: #{s}")
	end
end

class Array
  def stable_sort_by
    n = 0
    sort_by {|x| n+= 1; [yield(x), n]}
  end
end


if __FILE__ == $0
	opts,args = ARGV.partition {|text| text[0] == ?- }
	args << 'update' if args.empty?
	command = args.shift.downcase

	updater = Updater.new(File.dirname(__FILE__),opts)

	case command
	when 'update'
		updater.update_mods(args)
	when 'add'
		raise "Must specify mods to add" if args.empty?
		updater.add_mods(args)
	when 'delete'
		raise "Must specify mods to delete" if args.empty?
		updater.delete_mods(args)
	else
		puts "Valid commands: update add delete"
	end
end

WoW auction house search library

Library to search WoW auction houses using the www.auctionwowhouse.com site's web service
Example usage;
eldre = Warcraft::AuctionHouse.new("Eldre'Thalas","Alliance")
gems = eldre.query("solid star of elune", :sort => 'bid', :order => 'asc')
puts gems[0] unless gems.empty?

[Solid Star of Elune]: 65g/70g (14:15 left)
require 'net/http'
require 'delegate'

module Warcraft
  # Stores details of an auction
  Auction = Struct.new(:name,:quality,:quantity,:seller,:bid,:buyout,:time) unless Auction
  class Auction
    def to_s
      times = if quantity > 1 then "x#{quantity}" end
      bo = if buyout then "/#{buyout}" end
      "[#{name}]#{times}: #{bid}#{bo} (#{time} left)"
    end
  end
  CategoryInfo = 
<<-EOF.strip
weapon
 1h axe
 2h axe
 bow
 gun
 1h mace
 2h mace
 polearm
 1h sword
 2h sword
 staff
 fist
 misc
 dagger
 thrown
 crossbow
 wand
 fishing pole
armor
 miscellaneous
 cloth
 leather
 mail
 plate
 shield
 libram
 idol
 totem
container
 bag
 soul bag
 herb bag
 enchanting bag
 engineering bag
 gem bag
 mining bag
consumable
trade good
projectile
 arrow
 bullet
quiver
 quiver
 ammo pouch
recipe
 book
 leather
 tailor
 engineering
 blacksmithing
 cooking
 alchemy
 first aid
 enchanting
 jewelcrafting
reagent
misc
EOF
  ItemClasses=Hash.new {|h,k| raise "Unknown class #{k}"}
  ItemSubclasses=Hash.new {|h,k| raise "Unknown subclass #{k}"}
  ItemHierarchy = [ItemClasses,ItemSubclasses]
  ItemClasses[nil]=ItemSubclasses[nil]=nil
  CategoryInfo.each_with_index {|line,i|
    line =~ /^( *)(\w.*?)(\s*)$/
    ItemHierarchy[$1.length][$2]=i+1
  }
  Qualities = {
    "common" => 1,
    "white" => 1,
    "uncommon" => 2,
    "green" => 2,
    "rare" => 3,
    "blue" => 3,
    "epic" => 4,
    "purple" => 4
  }
  Sorting = {
    "name" => 6,
    "level" => 2,
    "time" => 3,
    "seller" => 7,
    "bid" => 4,
    "price" => 4
  }
  Order = {
    "ascending" => 0,
    "asc" => 0,
    "desc" => 1,
    "descending" => 1
  }
  
  # Represents an AH to query. 
  class AuctionHouse
    # e.g. AuctionHouse.new("Eldre'Thalas","Alliance")
    # Neutral AH are not available
    # Third parameter is 'EU' if you're on a european server.
    def initialize(realm,faction,locale="US")
      @realm,@locale = realm,locale
      @faction = case faction.to_s.downcase
      when "alliance"
        "Alliance"
      when "horde"
        "Horde"
      end
    end
    
    # Main entry point.
    # Usage: query("item name", :opt1 => "value", ...)
    # Item name can be nil. 
    # Options are:
    #  :min     minimum level item
    #  :max     maximum level item
    #  :quality white/green/blue/purple minimum item quality
    #  :type    weapon/armor/consumable/trade good/recipe etc
    #  :subtype 1h axe/enchanting/soul bag etc - must have the correct type specified too
    #  :seller  seller to search for
    #  :sort    name/level/time/bid/seller sorting method
    #  :order   asc/desc sort order
    #  :page    which page of results (1-based). 10 returned at a time.
    # Returns an array of Warcraft::Auctions.
    def query(name, opts={})
      params = {}
      params["realm"] = "#@realm #@locale" #"Eldre'Thalas US"
      params["faction"] = if @faction=="Alliance" then 1 else 2 end
      params["ItemName"] = name
      params["LevelStart"] = opts[:min] # Level range to filter by
      params["LevelEnd"] = opts[:max]
      params["Seller"] = opts[:seller]
      params["Rarity"] = Qualities[opts[:quality]]
      params["itemClassID"] = ItemClasses[opts[:type]] 
      params["itemSubClass"] = ItemSubclasses[opts[:subtype]] 
      params["pagenum"] = (opts[:page] || 1).to_s
      params["invenTypeID"] = nil # think this is the slot for armor searches - e.g. armor -> leather -> *shoulder*. unimplemented.
      params["sort_column"] = Sorting[opts[:sort]]
      params["sort_order"] = Order[opts[:order]]
      
      querytext = MiniJSON::encode(params)
      
      puts "Query:\n#{querytext}" if $DEBUG
      
      retrieve_results(querytext)
    end
    
    def retrieve_results(querytext)
      # We just post the JSON to the given URL and get a JSON encoded string back
      req = Net::HTTP::Post.new("/AuctionInfo.asmx/getAuctionInfo")
      req["Content-Type"]="application/json"
      response = Net::HTTP.new("www.auctionwowhouse.com",80).start {|http| http.request(req,querytext)}
      response.error! unless Net::HTTPSuccess === response
      resulttext = response.body
      resulttext = MiniJSON::decode(resulttext)
      puts "Response:\n#{resulttext}" if $DEBUG
      
      parse_results(resulttext)
    end
    
    # Web service returns a blob of ugly html, so we scrape the data out of it. Cover your eyes...
    def parse_results(resulttext)
      items = []

      resulttext.scan(/<TR style='height.*?>(.*?)<\/TR>/) {|text,| # A row of the main table containing an item
        puts "Item:\n#{text}" if $DEBUG
        item = Auction.new
        text.scan(/<font color=(.*?)>(.*?)<\/font>/) {|color,name|
          item.quality = color
          item.name = name
        }
        text.scan(/<td width="81" .*?>(.*?)<\/td>/) {|name,|
          item.seller = name
        }
        text.scan(/<td width="75" .*?>(.*?)<\/td>/) {|q,|
          item.quantity = q.to_i
        }
    		text.scan(/<td width="82" .*?><div .*?>(.*?)<\/div><\/td>/) {|time,|
    		  item.time = Duration.new(time.to_i)
  		  }
  		  text.scan(%r{(?:(\d+)<img border=0 src="/images/wow_40_jin\.gif">)?(?:(\d+)<img border=0 src="/images/wow_42_yin\.gif">)?(\d+)<img border=0 src="/images/wow_44_tong\.gif"><BR>(?:(?:(\d+)<img border=0 src="/images/wow_40_jin\.gif">)?(?:(\d+)<img border=0 src="/images/wow_42_yin\.gif">)?(\d+)<img border=0 src="/images/wow_44_tong\.gif">)?</a>}) {|bg,bs,bc,g,s,c|
          item.bid = Price.new(10000*bg.to_i + 100 * bs.to_i + bc.to_i)
          item.buyout = Price.new(10000*g.to_i + 100 * s.to_i + c.to_i) if g #buyout may be absent
        }
        items << item
      }
      
      items
    end
  end
  
  # Chunk of money (gold/silver/copper)
  class Price < DelegateClass(Integer); end unless Price # So we can reload the library without throwing
  class Price
    def gold; self / 10000; end
    def silver; (self / 100)%100; end
    def copper; self % 100; end
    def to_a; [gold,silver,copper]; end
    def inspect; "<#{self.class} #{self.to_i}>"; end
    
    # to display we pick the dominant unit (gold if it's at least 1g, else silver if it's at 
    # least 1s, else copper) and round to the nearest
    def to_s
      big,little,letter = 
        if gold > 0
          [gold,silver,"g"]
        elsif silver > 0
          [silver,copper,"s"]
        else
          [copper,0,"c"]
        end
      big += 1 if little >= 50
      "#{big}#{letter}" 
    end
    
  end
  
  # Measure of time (hours-minutes-seconds)
  class Duration < DelegateClass(Integer); end unless Duration
  class Duration
    def hours; self / 3600; end
    def minutes; (self / 60)%60; end
    def seconds; self % 60; end
    def to_a; [hours,minutes,seconds] end
    def to_s; "%d:%02d" % self.to_a; end # just show hours and minutes
    def inspect; "<#{self.class} #{to_s}>"; end
  end
  
  #Bare minimum JSON support to talk to the service
  module MiniJSON
    # Encode an object - we only support strings, numbers, and hashes
    def self.encode(x)
      case x
      when String
        "\"#{x.gsub('"','\\"')}\""
      when Numeric
        x.to_s
      when Hash
        "{" + x.map{|k,v| encode(k)+": "+encode(v)}.join(", ") + "}"