in-out, in-out (2)
そんなわけで、バックアップがてらコピペしてみる。
たぶん、Ruby 1.8 系列なら動くとは思うけど、微妙に Windows に依存しているというか環境変数名が Windows 用かも地味にバグフィックスするついでに Windows 以外でも対応しやすいようにしたつもりだけど厳密にやるのも面倒なので適当*1。コメントもないし変数名もほげほげであったりと微妙だけど、ま、そこそこ動けばいいやという気分。
特に需要もないだろうけど、一応、ライセンスは修正BSDみたいな感じ。
#!/usr/bin/env ruby # Copyright (C) 2006 yyuu <peek824545+hatena@gmail.com> require "csv" require "parsedate" require "shellwords" class M2Entry def initialize(amount, time=Time.new) @amount = amount @time = time @tags = Array.new @comment = "" end attr_accessor :amount attr_accessor :time attr_accessor :comment attr_reader :tags def add_tag(tag) unless @tags.include?(tag) @tags.push(tag) else nil end end def add_tags(*tags) added = Array.new tags.each { |tag| if add_tag(tag) added.push(tag) end } added end def to_a() ary = Array.new ary[0] = time.strftime("%Y/%m/%d %X") ary[1] = amount ary[2] = @tags.join("/") unless @tags.empty? ary[3] = @comment ary end def to_s() sprintf("%s %-15s %6d \# %s", @time.strftime("%Y/%m/%d"), @tags.join("/"), @amount, @comment) end end class M2Store M2STORE_PATH = "." class M2StoreError < StandardError end class M2StoreDateError < M2StoreError end def initialize(time=nil) @store = Array.new @time = time update end # attr_accessor :time attr_reader :time def time=(time) @time = time update time end def bound?() @time && true end def bound_filename() if bound? File.join(M2STORE_PATH, @time.strftime("mm%Y%m.csv")) else nil end end def update() if bound? reload_db else true end end def reload() reload_db end def commit() write_db end def add_entry(entry) entry.is_a?(M2Entry) or raise("not my buisiness: #{e.class.to_s}") if @time and \ (@time.year != entry.time.year or @time.mon != entry.time.mon) raise(M2DateError.new("month unmatch")) end @store.push(entry) end def to_a() @store.dup end def each(&block) @store.each(&block) end def |(other) @store | other.to_a end def &(other) @store & other.to_a end def match_tag(tag) matched = self.class.new @store.each { |entry| matched.add_entry(entry) if entry.tags.include?(tag) } matched end def match_tags(*tags) # matched = self.class.new # tags.each { |tag| # matched |= match_tag(tag) # } # matched car, *cdr = tags _store = match_tag(car) if cdr.empty? _store else _store.match_tags(cdr) end end def unmatch_tag(tag) unmatched = self.class.new @store.each { |entry| unmatched.add_entry(entry) unless entry.tags.include?(tag) } unmatched end def unmatch_tags(*tags) car, *cdr = tags _store = unmatch_tag(car) if cdr.empty? _store else _store.unmatch_tags(cdr) end end private def clear() @store.clear end def reload_db() clear fcount = 0 begin CSV.open(bound_filename, "r") { |a| add_entry(a2e(a)) } rescue Errno::ENOENT => e p(e) if fcount < 1 fcount += 1 File.open(bound_filename, "w").close retry end end end def write_db() fcount = 0 begin CSV.open(bound_filename, "w") { |writer| @store.each { |entry| writer << e2a(entry) } } rescue Errno::ENOENT => e p(e) if fcount < 1 fcount += 1 File.open(bound_filename, "w").close retry end end end def a2e(ary) time = Time.local(*ParseDate.parsedate(ary[0])) amount = ary[1].to_i tags = ary[2].split("/") comment = ary[3] entry = M2Entry.new(amount, time) entry.add_tags(*tags) entry.comment = comment entry end def e2a(entry) entry.to_a end end class M2 COMMAND = Hash.new case RUBY_PLATFORM when /mswin32/ USERNAME = ENV["USERNAME"].downcase HOSTNAME = ENV["COMPUTERNAME"].downcase when /linux/ USERNAME = ENV["USERNAME"] HOSTNAME = ENV["HOSTNAME"] else USERNAME = "somebody" HOSTNAME = "somewhere" end def initialize() @m2store = M2Store.new(Time.new) end def start() pstr = sprintf("[%s@%s]$ ", USERNAME, HOSTNAME) loop { if line = prompt(pstr, true) if /(?!\\)\#(.*)$/ =~ line # line includes comments cmdline, comment = $`, $& else cmdline, comment = line, "" end sw = Shellwords.shellwords(cmdline) cmd, *args = sw if cmd and not(cmd.empty?) if has_command?(cmd) call_command(cmd, args) else STDERR.printf("Command not found: %s\n", cmd) end end end } end private begin require "readline" def prompt(pstr, history=true) return Readline.readline(pstr, history) end rescue LoadError => e def prompt(pstr, *rest) STDOUT.printf(pstr) STDOUT.flush return STDIN.gets end end def has_command?(cmd) get_command(cmd) && true end def call_command(cmd, args) get_command(cmd).call(args, @m2store) end def get_command(cmd) COMMAND[cmd] end end M2::COMMAND["exit"] = \ M2::COMMAND["quit"] = \ Proc.new { |args, store| exit(0) } M2::COMMAND["cd"] = \ Proc.new{ |args, store| unless args.empty? car, *cdr = args if /^((\d{4})\/)?(\d{1,2})$/ =~ car now = Time.new year = ($1.nil?) ? now.year : $1.to_i mon = $3.to_i if store.time.mon != mon store.time = Time.local(year, mon) end else STDERR.puts("No such directory: #{car}") end else time = Time.new if store.time.mon != time.mon store.time = time end end } M2::COMMAND["ls"] = \ M2::COMMAND["grep"] = \ Proc.new { |args, store| _store = store unless args.empty? car, *cdr = args if /^((\d{4})\/)?(\d{1,2})$/ =~ car now = Time.new year = ($1.nil?) ? now.year : $1.to_i mon = $3.to_i if store.time.mon != mon # store.time = Time.local(year, mon) _store = M2Store.new(Time.local(year, mon)) end else cdr.unshift(car) end tags_i = cdr.find_all { |e| e[0] == ?- } || [] # invert tag tags_n = cdr - tags_i # normal tag tags_i.collect! { |e| e[1..-1] } unless tags_n.empty? _store = _store.match_tags(*tags_n) end unless tags_i.empty? _store = store.unmatch_tags(*tags_i) end end _store.each { |entry| STDOUT.puts(entry) } } M2::COMMAND["calc"] = \ Proc.new { |args, store| _store = store unless args.empty? car, *cdr = args if /^((\d{4})\/)?(\d{1,2})$/ =~ car now = Time.new year = ($1.nil?) ? now.year : $1.to_i mon = $3.to_i _store = M2Store.new(Time.local(year, mon)) else cdr.unshift(car) end tags_i = cdr.find_all { |e| e[0] == ?- } || [] # invert tag tags_n = cdr - tags_i # normal tag tags_i.collect! { |e| e[1..-1] } unless tags_n.empty? _store = _store.match_tags(*tags_n) end unless tags_i.empty? _store = _store.unmatch_tags(*tags_i) end end sum = _store.to_a.inject(0) { |s, entry| s + entry.amount } STDOUT.puts(sum.to_s) } M2::COMMAND["add"] = \ M2::COMMAND["insert"] = \ Proc.new { |args, store| amount = nil comment = "" time = Time.new tags = Array.new while arg = args.shift case arg when /^--date$/, /^-d$/ d_str = args.shift if /^((\d{1,2})\/)?(\d{1,2})$/ =~ d_str now = Time.new mon = ($1.nil?) ? now.year : $1.to_i day = $3.to_i time = Time.local(now.year, mon, day) else STDERR.puts("insert: invalid date format: #{d_str}") end when /^--tag$/, /^-t$/ t_str = args.shift tags |= t_str.split(/[,\/]/) when /^--comment$/, /^-c$/ c_str = args.shift comment << c_str when /^\d+$/ amount = arg.to_i else if amount.nil? tags |= [arg] else STDERR.puts("insert: unknown option -- #{arg}") break end end end unless amount.nil? entry = M2Entry.new(amount, time) entry.add_tags(*tags) entry.comment = comment begin store.add_entry(entry) store.commit # automatic commiting rescue M2DateError => e STDERR.puts("insert: out of directory") end else STDERR.puts("insert: amount not specified") end } M2::COMMAND["commit"] = \ Proc.new { |args, store| store.commit } M2::COMMAND["du"] = \ M2::COMMAND["wc"] = \ Proc.new { |args, store| table = Hash.new(0) store.each { |entry| entry.tags.each { |tag| sum = table[tag] || 0 table[tag] += entry.amount } } table.sort { |a, b| b[1] <=> a[1] }.each { |e| STDOUT.printf("%10d %s\n", e[1], e[0]) } } M2::COMMAND["hist"] = \ M2::COMMAND["histogram"] = \ Proc.new { |args, store| table = Hash.new(0) store.each { |entry| entry.tags.each { |tag| sum = table[tag] || 0 table[tag] += entry.amount } } table.sort { |a, b| b[1] <=> a[1] }.each { |e| STDOUT.printf("%10s %s\n", e[0], "#" * (e[1].to_f/500.0)) } } M2::COMMAND["exec"] = \ Proc.new { |args, store| system(args.join(" ")) } M2::COMMAND["date"] = \ Proc.new { |args, store| STDOUT.puts(store.time) } M2.new.start