moneybook 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/VERSION +1 -1
  2. data/bin/moneybook +177 -134
  3. metadata +4 -4
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6
1
+ 0.1.7
@@ -1,25 +1,48 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'rubygems'
3
- require 'iconv'
3
+ ###
4
4
  require 'terminal-table/import'
5
- require 'commander/import'
6
- require 'date'
7
5
  require 'andand'
6
+ require 'highline/import'
7
+ ###
8
+ require 'iconv'
9
+ require 'optparse'
10
+ require 'date'
8
11
  require 'abbrev'
9
12
  require 'pp'
10
13
 
11
- # :name is optional, otherwise uses the basename of this executable
12
- program :name, 'MoneyBook'
13
- program :version, '0.1'
14
- program :description, 'A simple program to manage group expenses, for example a trip with friends...'
14
+ class Array
15
+ def normalize_length
16
+ max = self.inject{|longest, current| longest.to_s.length > current.to_s.length ? longest : current }.to_s.length
17
+ self.map{|x| x = "%-#{max}s" % x.to_s}
18
+ end
19
+ end
20
+
21
+ def process_command(cmd)
22
+ args = Array(cmd)
23
+ command = args.shift
24
+ case(command)
25
+ when "new"
26
+ command_new(args[0])
27
+ when "parse"
28
+ options = {}
29
+ OptionParser.new do |opts|
30
+ opts.banner = "Usage: moneybook parse [options] FILENAME"
31
+ opts.on("-i", "--[no-]intermediate", "Show intermediate computations") do |i|
32
+ options[:intermediate] = i
33
+ end
34
+ end.parse!(args)
35
+ command_parse(options, args.last)
36
+ else
37
+ puts "unknown command: #{command}"
38
+ end
39
+ end
15
40
 
16
- command :new do |c|
17
- c.syntax = 'moneybook new'
18
- c.description = 'create a skeleton moneybook file'
19
- c.action do |args, options|
20
- title = ask("Title: ")
21
- people = ask('People (separated by spaces): ', lambda{|x|x.split(/\s+/)})
22
- text = "### #{title} ###
41
+ def command_new(title)
42
+ title = ask("Title: "){|q| q.readline = true} unless title.to_s != ''
43
+ people = ask('People (separated by spaces): ', lambda{|x|x.split(/\s+/)}){|q| q.readline = true}
44
+ change= ask("Currency change? (leave blank if not needed) ", Float) { |q| q.default = 0; q.readline = true }
45
+ text = "### #{title} ###
23
46
  # #{Date.today.to_s}
24
47
  #######
25
48
  # - everyone is included:
@@ -32,152 +55,172 @@ command :new do |c|
32
55
  # dinner (50Fra 16Tom) +5Tom -2Jack -Mary #Tom spends 5 more than the others, Jack 2 less
33
56
  # - synctactic sugar, instead of ...
34
57
  # pay back (20L) T
35
- # - ... you can write
58
+ # - ... you can write ...
36
59
  # 20L -> T
60
+ # - ... and since it's a payback it won't count on the spent amount
37
61
  #######
38
62
 
39
63
  PEOPLE: #{people.join(' ')}"
40
-
41
- filename = "moneybook_#{title.downcase.gsub(' ','_')}.txt"
42
- if File.open(filename,'w'){|f|f.write text} then
43
- puts "File #{filename} created!"
44
- else
45
- puts "Could not create file #{filename}.."
46
- end
64
+ text += "\nCHANGE: #{change}" if change > 0
65
+ filename = "moneybook_#{title.downcase.gsub(' ','_')}.txt"
66
+ if File.open(filename,'w'){|f|f.write text} then
67
+ puts "File #{filename} created!"
68
+ else
69
+ puts "Could not create file #{filename}.."
47
70
  end
48
71
  end
49
72
 
50
73
  def get_final(sq, people)
51
74
  final = {}
52
- people.each{|person| final[person]=0;}
75
+ spent = {}
76
+ given = {}
77
+ people.each{|person| final[person]=0;spent[person]=0;given[person]=0}
53
78
  sq.each {|c|
54
79
  c[:balance].each_pair{|person, value|
55
80
  final[person] += value
56
81
  }
82
+ c[:virtual].each_pair{|person, value|
83
+ spent[person] += value unless c[:name] == 'PAYBACK'
84
+ }
85
+ c[:true].each_pair{|person, value|
86
+ given[person] += value
87
+ }
57
88
  }
58
- final
89
+ [final, spent, given]
59
90
  end
60
91
 
61
- command :parse do |c|
62
- c.syntax = 'moneybook parse [options] [file]'
63
- c.description = 'calculate total debts for everyone'
64
- c.action do |args, options|
65
- intermediate = agree("Do you want to see the intermediate results? ")
66
- people = []
67
- people_abbreviations = []
68
- sq = [] #big table to make the computations..
69
- f=File.open(args.first,'r').read
70
- if f[0]==255 && f[1]==254 then
71
- f=Iconv.iconv('UTF-8', 'UTF-16LE', f)[0][3..-1]
72
- puts "converting from UTF-16LE..."
73
- end
74
- f.each_line{|line|
75
- if line =~ /^\s*\#/ || line =~ /\A\s*\Z/
76
- # this is interpreted as a comment line
77
- 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
78
- m.each{|z|people<<z.to_s.downcase}
79
- people_abbreviations = people.abbrev
80
- else
81
- original_str = false
82
- if temp=line.match(/\A\s*(.+)\s*\-+\>\s*(.+)\Z/) then
83
- original_str = line.strip
84
- line = "PAYBACK (#{temp[1].to_s}) #{temp[2].to_s}"
85
- end
86
- x = line.strip.match(/^\s*([\w\ \']+?)\s*\((.+)\)\s*(.+)?/).to_a.map{|z| z.to_s}
87
- 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 => {},
88
- :true_total => 0, :virtual_total => 0}
89
- c=sq.last
90
- people.each{|person| c[:true][person]=0;c[:virtual][person]=0;c[:balance][person]=0}
91
-
92
- # parsing of true_str
93
- c[:true_str].split(/\s+/).each{|y|
94
- temp = y.match(/([\d\.]+)([A-Za-z]+)/)
95
- if (person=people_abbreviations[temp[2].to_s.downcase]) != nil
96
- c[:true][person] += temp[1].to_f
97
- c[:true_total] += temp[1].to_f
98
- else
99
- puts "error: ambiguos or inexistent name: #{temp[2].to_s}"
100
- exit
101
- end
102
- }
92
+ def command_parse(options, filename)
93
+ intermediate = options[:intermediate]
94
+ people = []
95
+ currency_change = 0
96
+ people_abbreviations = []
97
+ sq = [] #big table to make the computations..
98
+ f=File.open(filename,'r').read
99
+ if f[0]==255 && f[1]==254 then
100
+ f=Iconv.iconv('UTF-8', 'UTF-16LE', f)[0][3..-1]
101
+ puts "converting from UTF-16LE..."
102
+ end
103
+ f.each_line{|line|
104
+ if line =~ /^\s*\#/ || line =~ /\A\s*\Z/
105
+ # this is interpreted as a comment line
106
+ puts line if intermediate
107
+ 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
108
+ m.each{|z|people<<z.to_s.downcase}
109
+ people_abbreviations = people.abbrev
110
+ elsif (currency_change == 0) && ((m = line.match(/\A\s*CHANGE\s*\:\s*([\.\d]+)\s*\Z/).andand[1].to_f) != 0) then
111
+ currency_change = m
112
+ puts "currency change is #{currency_change}"
113
+ else
114
+ original_str = false
115
+ if temp=line.match(/\A\s*(.+)\s*\-+\>\s*(.+)\Z/) then
116
+ original_str = line.strip
117
+ line = "PAYBACK (#{temp[1].to_s}) #{temp[2].to_s}"
118
+ end
119
+ x = line.strip.match(/^\s*([\w\ \']+?)\s*\((.+)\)\s*(.+)?/).to_a.map{|z| z.to_s}
120
+ 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 => {},
121
+ :true_total => 0, :virtual_total => 0}
122
+ c=sq.last
123
+ people.each{|person| c[:true][person]=0;c[:virtual][person]=0;c[:balance][person]=0}
103
124
 
104
- # parsing of virtual_str
105
- if c[:virtual_str] =~ /\A\s*\Z/ then
106
- c[:virtual].each_key{|person|c[:virtual][person] = c[:true_total]/people.length}
107
- c[:virtual_total] = c[:true_total]
125
+ # parsing of true_str
126
+ c[:true_str].split(/\s+/).each{|y|
127
+ temp = y.match(/([\d\.]+)([A-Za-z]+)/)
128
+ if (person=people_abbreviations[temp[2].to_s.downcase]) != nil
129
+ c[:true][person] += temp[1].to_f
130
+ c[:true_total] += temp[1].to_f
108
131
  else
109
- avoid = []
110
- people_to_complete = []
111
- complete_automatically = true
112
- c[:virtual_str].split(/\s+/).each{|y|
113
- if (temp = y.match(/([\+\-\d\.]*)([A-Za-z]+)/)) then
114
- if (person=people_abbreviations[temp[2].to_s.downcase]) != nil
115
- if temp[1].to_s == "" then
116
- complete_automatically = false
117
- people_to_complete << person
132
+ puts "error: ambiguos or inexistent name: #{temp[2].to_s}"
133
+ exit
134
+ end
135
+ }
136
+
137
+ # parsing of virtual_str
138
+ if c[:virtual_str] =~ /\A\s*\Z/ then
139
+ c[:virtual].each_key{|person|c[:virtual][person] = c[:true_total]/people.length}
140
+ c[:virtual_total] = c[:true_total]
141
+ else
142
+ avoid = []
143
+ people_to_complete = []
144
+ complete_automatically = true
145
+ c[:virtual_str].split(/\s+/).each{|y|
146
+ if (temp = y.match(/([\+\-\d\.]*)([A-Za-z]+)/)) then
147
+ if (person=people_abbreviations[temp[2].to_s.downcase]) != nil
148
+ if temp[1].to_s == "" then
149
+ complete_automatically = false
150
+ people_to_complete << person
151
+ avoid << person
152
+ elsif temp[1].to_s[0,1] == '+'
153
+ c[:virtual][person] += temp[1].to_s[1..-1].to_f
154
+ c[:virtual_total] += temp[1].to_f
155
+ elsif temp[1].to_s[0,1] == '-'
156
+ if temp[1].to_s == '-' then
157
+ c[:virtual][person] = 0
118
158
  avoid << person
119
- elsif temp[1].to_s[0,1] == '+'
120
- c[:virtual][person] += temp[1].to_s[1..-1].to_f
121
- c[:virtual_total] -= temp[1].to_f
122
- elsif temp[1].to_s[0,1] == '-'
123
- if temp[1].to_s == '-' then
124
- c[:virtual][person] = 0
125
- avoid << person
126
- else
127
- c[:virtual][person] -= temp[1].to_s[1..-1].to_f
128
- c[:virtual_total] += temp[1].to_f
129
- end
130
159
  else
131
- c[:virtual][person] += temp[1].to_f
132
- c[:virtual_total] += temp[1].to_f
133
- avoid << person
160
+ c[:virtual][person] -= temp[1].to_s[1..-1].to_f
161
+ c[:virtual_total] -= temp[1].to_f
134
162
  end
135
163
  else
136
- puts "error: ambiguos or inexistent name: #{temp[2].to_s}"
137
- exit
164
+ c[:virtual][person] += temp[1].to_f
165
+ c[:virtual_total] += temp[1].to_f
166
+ avoid << person
138
167
  end
168
+ else
169
+ puts "error: ambiguos or inexistent name: #{temp[2].to_s}"
170
+ exit
139
171
  end
140
- }
141
- if c[:virtual_total] > c[:true_total]
142
- puts "error: Virtual total doesn't match.."
143
- exit
144
172
  end
145
- if complete_automatically then
146
- others = c[:virtual].map{|i,v|i}-avoid
147
- others.each { |p|
148
- c[:virtual][p] += (c[:true_total] - c[:virtual_total])/others.length
149
- }
150
- else
151
- people_to_complete.each { |p|
152
- c[:virtual][p] += (c[:true_total] - c[:virtual_total])/people_to_complete.length
153
- }
154
- end
155
- end
156
- c[:balance].each_key{|person|
157
- c[:balance][person] = c[:true][person] - c[:virtual][person]
158
173
  }
159
- ### PRINT INTERMEDIATE RESULTS ###
160
- if intermediate then
161
- puts "==== #{c[:original_str].to_s} ===="
162
- puts "total: #{c[:true_total].to_s}"
163
- mytable = table do |t|
164
- t.headings = people.sort
165
- t << c[:true].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
166
- t << c[:virtual].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
167
- t.add_separator
168
- t << c[:balance].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
169
- t.add_separator
170
- final = get_final(sq, people)
171
- t << final.sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
172
- end
173
- puts mytable
174
+ if c[:virtual_total] > c[:true_total]
175
+ puts "error: Virtual total doesn't match.."
176
+ exit
177
+ end
178
+ if complete_automatically then
179
+ others = c[:virtual].map{|i,v|i}-avoid
180
+ others.each { |p|
181
+ c[:virtual][p] += (c[:true_total] - c[:virtual_total])/others.length
182
+ }
183
+ else
184
+ people_to_complete.each { |p|
185
+ c[:virtual][p] += (c[:true_total] - c[:virtual_total])/people_to_complete.length
186
+ }
174
187
  end
175
- ###
176
188
  end
177
- }
178
- ### PRINT FINAL RESULTS ###
179
- get_final(sq, people).each_pair {|person, value|
180
- puts "#{person} \t #{value > 0 ? 'receives' : 'gives '} #{"%.1f" % value}"
181
- }
182
- end
189
+ c[:balance].each_key{|person|
190
+ c[:balance][person] = c[:true][person] - c[:virtual][person]
191
+ }
192
+ ### PRINT INTERMEDIATE RESULTS ###
193
+ if intermediate then
194
+ puts "==== #{c[:original_str].to_s} ===="
195
+ puts "total: #{c[:true_total].to_s}"
196
+ mytable = table do |t|
197
+ t.headings = people.sort
198
+ t << c[:true].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
199
+ t << c[:virtual].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
200
+ t.add_separator
201
+ t << c[:balance].sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
202
+ t.add_separator
203
+ final = get_final(sq, people)[0]
204
+ t << final.sort.map{|z|z[1] == 0 ? "" : "%.1f" % z[1]}
205
+ end
206
+ puts mytable
207
+ end
208
+ ###
209
+ end
210
+ }
211
+ ### PRINT FINAL RESULTS ###
212
+ final = get_final(sq, people)
213
+ puts "in local currency:" if currency_change > 0
214
+ people_to_print = people.normalize_length
215
+ people.each_index{|i|
216
+ person = people[i]
217
+ puts "#{people_to_print[i]} \t #{final[0][person] > 0 ? 'receives' : 'gives '} #{"% 8.2f" % final[0][person]} spent #{"% 8.2f" % final[1][person]} given #{"% 8.2f" % final[2][person]}"
218
+ }
219
+ puts "in converted currency:" if currency_change > 0
220
+ people.each_index{|i|
221
+ person = people[i]
222
+ puts "#{people_to_print[i]} \t #{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)}" if currency_change > 0
223
+ }
183
224
  end
225
+
226
+ process_command(ARGV)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moneybook
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 21
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 6
10
- version: 0.1.6
9
+ - 7
10
+ version: 0.1.7
11
11
  platform: ruby
12
12
  authors:
13
13
  - luca cioria
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-11 00:00:00 +01:00
18
+ date: 2011-01-18 00:00:00 +01:00
19
19
  default_executable: moneybook
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency