moneybook 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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