Top Level Namespace

Defined Under Namespace

Classes: Array

Constant Summary collapse

REGEXP_COMP_LINE =
/^\s*([\w\ \']+?)\s*\((.+)\)\s*(.+)?/
REGEXP_PAYBACK_LINE =
/\A\s*(.+)\s*\-+\>\s*(.+)\Z/
REGEXP_DEBT_LINE =
/\A\s*(.+)\s*\@\s*(.+)\Z/

Instance Method Summary collapse

Instance Method Details

#command_new(title) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'bin/moneybook', line 53

def command_new(title)
  title = ask("Title: "){|q| q.readline = true} unless title.to_s != ''
  people = ask('People (separated by spaces): ', lambda{|x|x.split(/\s+/)}){|q| q.readline = true}
  change= ask("Currency change? (leave blank if not needed)  ", Float) { |q| q.default = 0; q.readline = true }
  text = "### #{title} ###
# #{Date.today.to_s}
#######
# - everyone is included:
# pizza (45Luca)
# - only M and L are included because they sum up to the total
# cinema (30Lu) 12M 18L #names can be abbreviated
# - everyone is included because T doesn't reach the total
# cinema (30Mark) 7T
# - everyone but Mary is included
# dinner (50Fra 16Tom) +5Tom -2Jack -Mary #Tom spends 5 more than the others, Jack 2 less
# - synctactic sugar, instead of ...
# pay back (20L) T
# - ... you can write ...
# 20L -> T
# - ... and since it's a payback it won't count on the spent amount
# - another synctactic sugar, instead of ...
# undefined past debt (20T) L
# - ... you can write ...
# 20L @ T
# which means the opposite of 20L -> T
# it means that L owes T 20$
# and it doesn't count on the spent amount
#
# lines beginning with # are comments, you can also add comments at the end of a line:
# pizza (45 Luca) 10 Tommaso # Tommaso didn't get the drink..
#
# expressions between dollar signs will be evaluated, like so:
# pizza (45 Luca) $ 5+3.5+2*0.7$ Tommaso
#######

PEOPLE: #{people.join(' ')}"
  text += "\nCHANGE: #{change}" if change > 0
  filename = "money_#{title.downcase.gsub(' ','_')}.txt"
  if File.open(filename,'w'){|f|f.write text} then
    puts "File #{filename} created!"
  else
    puts "Could not create file #{filename}.."
  end
end

#command_parse(options, filename) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'bin/moneybook', line 176

def command_parse(options, filename)
  intermediate = options[:intermediate]
  people = []
  currency_change = 0
  people_abbreviations = []
  sq = [] #big table to make the computations..
  f=File.open(filename,'r').read
  ### in case it's a UTF-16LE file...
  if f[0]==255 && f[1]==254 then
    f=Iconv.iconv('UTF-8', 'UTF-16LE', f)[0][3..-1]
    puts "converting from UTF-16LE..."
  end
  ### count number of computational lines (for the --tail option)
  number_of_computational_lines = f.to_a.count{|x|x =~ REGEXP_PAYBACK_LINE || x =~ REGEXP_COMP_LINE}
  reached_tail = !options[:tail]
  current_comp_line = 0
  ### start computation
  f.each_line{|line|
    if line =~ /^\s*\#/ || line =~ /\A\s*\Z/
      # this is interpreted as a comment line
      puts line if intermediate && reached_tail
    elsif (m = line.match(/\A\s*PEOPLE\s*\:?\s*((\s+\w+)+)\s*\Z/).andand[1].andand.split(/\s+/).andand.delete_if{|x|x==""}) != nil then
      m.each{|z|people<<z.to_s.downcase}
      people_abbreviations = people.abbrev
      puts "[people in list: #{people.join(', ')}]" if intermediate
    elsif (currency_change == 0) && ((m = line.match(/\A\s*CHANGE\s*\:\s*([\.\d]+)\s*\Z/).andand[1].to_f) != 0) then
      currency_change = m
      puts "[currency change is #{currency_change}]" if intermediate
    else
      current_comp_line += 1
      reached_tail = true if options[:tail] && current_comp_line > number_of_computational_lines - options[:tail_number]
      original_str = false
      # eval math expressions in the form $3 + 4*12$
      line.gsub!(/\$([^\$]*)\$/){
        if intermediate && reached_tail then
          puts "#{line.strip} ==> calc: #{$1} = #{eval $1}"
        end
        eval $1
      }
      # clear any end of line comment and print them if needed
      temp=line.split('#')
      line=temp[0].strip
      puts "# "+temp[1..-1].join('#').strip if temp.length > 1 && intermediate && reached_tail
      ###
      if temp=line.match(REGEXP_PAYBACK_LINE) then
        original_str = line.strip
        line = "PAYBACK (#{temp[1].to_s}) #{temp[2].to_s}"
      end
      if temp=line.match(REGEXP_DEBT_LINE) then
        temp2 = temp[1].to_s.match(/([\d\.]+)\s*([A-Za-z]+)/)
        original_str = line.strip
        line = "DEBT (#{temp2[1]}#{temp[2].to_s}) #{temp2[2].to_s}"
      end      
      x = line.strip.match(REGEXP_COMP_LINE).to_a.map{|z| z.to_s}
      sq << {:name => x[1].to_s, :original_str => original_str || line.strip, :true_str => x[2].to_s, :virtual_str => x[3].to_s, :true => {}, :virtual => {}, :balance => {},
      :true_total => 0, :virtual_total => 0}
      c=sq.last
      people.each{|person| c[:true][person]=0;c[:virtual][person]=0;c[:balance][person]=0}

      # parsing of true_str
      c[:true_str].split(/\s+(?=\d)/).each{|y|
        temp = y.match(/([\d\.]+)\s*([A-Za-z]+)/)
        if (person=people_abbreviations[temp[2].to_s.downcase]) != nil
          c[:true][person] += temp[1].to_f
          c[:true_total] += temp[1].to_f
        else
          puts "error: ambiguos or inexistent name: #{temp[2].to_s}"
          exit
        end
      }

      # parsing of virtual_str
      if c[:virtual_str] =~ /\A\s*\Z/ then
        c[:virtual].each_key{|person|c[:virtual][person] = c[:true_total]/people.length}
        c[:virtual_total] = c[:true_total]
      else
        avoid = []
        people_to_complete = []
        complete_automatically = true
        c[:virtual_str].split(/\s+/).each{|y|
          if (temp = y.match(/([\+\-\d\.]*)([A-Za-z]+)/)) then         
            if (person=people_abbreviations[temp[2].to_s.downcase]) != nil
              if temp[1].to_s == "" then
                complete_automatically = false
                people_to_complete << person
                avoid << person
              elsif temp[1].to_s[0,1] == '+'
                c[:virtual][person] += temp[1].to_s[1..-1].to_f
                c[:virtual_total] += temp[1].to_f
              elsif temp[1].to_s[0,1] == '-'
                if temp[1].to_s == '-' then
                  c[:virtual][person] = 0
                  avoid << person
                else
                  c[:virtual][person] -= temp[1].to_s[1..-1].to_f
                  c[:virtual_total] -= temp[1].to_f
                end
              else
                c[:virtual][person] += temp[1].to_f
                c[:virtual_total] += temp[1].to_f
                avoid << person
              end
            else
              puts "error: ambiguos or inexistent name: #{temp[2].to_s}"
              exit
            end
          end
        }
        if c[:virtual_total] > c[:true_total]
          puts "error: Virtual total doesn't match.."
          exit
        end
        if complete_automatically then      
          others = c[:virtual].map{|i,v|i}-avoid
          others.each { |p|
            c[:virtual][p] += (c[:true_total] - c[:virtual_total])/others.length
          }
        else
          people_to_complete.each { |p|
            c[:virtual][p] += (c[:true_total] - c[:virtual_total])/people_to_complete.length
          }
        end
      end
      c[:balance].each_key{|person|
        c[:balance][person] = c[:true][person] - c[:virtual][person]
      }
      ### PRINT INTERMEDIATE RESULTS ###
      if intermediate && reached_tail then
        puts "==== #{"%2d: " % current_comp_line} #{c[:original_str].to_s} ===="
        puts "total: #{c[:true_total].to_s}"
        mytable = table do |t|
          t.headings = people.sort
          t << c[:true].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
          t << c[:virtual].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
          t.add_separator
          t << c[:balance].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
          t.add_separator
          final = get_final(sq, people)[0]
          t << final.sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
        end
        puts mytable
      end
      ###
    end
  }
  ### PRINT FINAL RESULTS ###
  final = get_final(sq, people)
  puts "=== in local currency:" if currency_change > 0
  people_to_print = people.normalize_length
  people.each_index{|i|
    person = people[i]
    puts "#{people_to_print[i]}  #{final[0][person] == 0 ? '---     ' : (final[0][person] > 0 ? 'receives' : 'gives   ')}   #{"% 8.2f" % final[0][person]} spent #{"% 8.2f" % final[1][person]} given #{"% 8.2f" % final[2][person]} debt #{"% 8.2f" % final[3][person]}"
  }
  puts "=== in converted currency (multiplying by #{currency_change}):" if currency_change > 0
  people.each_index{|i|
    person = people[i]
    puts "#{people_to_print[i]}  #{final[0][person] == 0 ? '---     ' : (final[0][person] > 0 ? 'receives' : 'gives   ')}   #{"% 8.2f" % (final[0][person]*currency_change)} spent #{"% 8.2f" % (final[1][person]*currency_change)} given #{"% 8.2f" % (final[2][person]*currency_change)} debt #{"% 8.2f" % final[3][person]*currency_change}" if currency_change > 0
  }
  ### PRINT MONEY EXCHANGES ###
  exchanges = get_exchanges(final,people)
end

#get_exchanges(final, people) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'bin/moneybook', line 121

def get_exchanges(final,people)
  poor_p = [] # people who must give..
  poor_v = []
  rich_p = [] # people who shall receive..
  rich_v = []
  final[0].each_pair{|person, value|
    if value > 0 then
       rich_p << person
       rich_v << value.round
    elsif value < 0 then
       poor_p << person
       poor_v << -value.round
    else
      # this person doesn't have to give or receive anything..
    end
  }
  out = []
  puts "\nPOSSIBLE (APPROX) EXCHANGES.."
  while (eq=rich_v-(rich_v-poor_v)) != [] do
    receives = rich_v.index eq[0]
    gives    = poor_v.index eq[0]
    amount   = eq[0]
    out << "#{poor_p[gives]} gives #{amount} to #{rich_p[receives]}"
    rich_p.delete_at receives
    rich_v.delete_at receives    
    poor_p.delete_at gives
    poor_v.delete_at gives    
  end
  timeout = 20
  while poor_v.length > 0 && rich_v.length > 0 && timeout > 0 do
    #pp "---"
    #pp [poor_v, rich_v]
    #pp [poor_p, rich_p]
    gives    = poor_v.index poor_v.max
    receives = rich_v.index rich_v.min
    amount   = poor_v.max >= rich_v.min ? rich_v.min : poor_v.max
    out << "#{poor_p[gives]} gives #{amount} to #{rich_p[receives]}"
    rich_v[receives] -= amount
    poor_v[gives]    -= amount
    #pp [poor_v, rich_v]
    #pp [poor_p, rich_p]    
    if poor_v[gives] == 0
      poor_p.delete_at gives
      poor_v.delete_at gives
    end
    if rich_v[receives] == 0
      rich_p.delete_at receives
      rich_v.delete_at receives
    end
    timeout -= 1
  end
  out.sort.each{|x|puts x}
  puts "--------------"
end

#get_final(sq, people) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'bin/moneybook', line 98

def get_final(sq, people)
  final = {}
  spent = {}
  given = {}
  debt  = {}
  people.each{|person| final[person]=0;spent[person]=0;given[person]=0;debt[person]=0;}
  sq.each {|c|
    c[:balance].each_pair{|person, value|
      final[person] += value
    }
    c[:virtual].each_pair{|person, value|
      spent[person] += value unless (c[:name] == 'PAYBACK' || c[:name] == 'DEBT')
    }
    c[:virtual].each_pair{|person, value|
      debt[person] += value if (c[:name] == 'PAYBACK' || c[:name] == 'DEBT')
    }    
    c[:true].each_pair{|person, value|
      given[person] += value
    }
  }
  [final, spent, given, debt]
end

#process_command(cmd) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'bin/moneybook', line 25

def process_command(cmd)
  args = Array(cmd)
  command = args.shift
  case(command)
  when "new"
    command_new(args[0])
  when "parse"
    options = {}
    OptionParser.new do |opts|
      opts.banner = "Usage: moneybook parse [options] FILENAME"
      opts.on("-i", "--[no-]intermediate", "Show intermediate computations") do |i|
        options[:intermediate] = i
      end
      opts.on("-t", "--tail NUMBER", Integer, "Show intermediate computations only on last NUMBER lines") do |t|
        options[:tail] = true
        options[:tail_number] = t
      end
      opts.on_tail("-h", "--help", "Show this message") do
        puts opts
        exit
      end
    end.parse!(args)
    command_parse(options, args.last)
  else
    puts "unknown command: #{command}.. available commmands: new, parse\ndo moneybook parse --help for more help"
  end
end