bell 0.0.1

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 (67) hide show
  1. data/.gitignore +5 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +47 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.org +128 -0
  7. data/Rakefile +16 -0
  8. data/bell.gemspec +30 -0
  9. data/bin/bell +5 -0
  10. data/features/create_user.feature +16 -0
  11. data/features/full_report.feature +84 -0
  12. data/features/import_public_contacts.feature +20 -0
  13. data/features/import_user_contacts.feature +43 -0
  14. data/features/list_all_contacts.feature +19 -0
  15. data/features/list_user_contacts.feature +27 -0
  16. data/features/list_users.feature +22 -0
  17. data/features/remove_user.feature +21 -0
  18. data/features/rename_user.feature +25 -0
  19. data/features/step_definitions/cli_steps.rb +59 -0
  20. data/features/step_definitions/contact_steps.rb +104 -0
  21. data/features/step_definitions/database_steps.rb +3 -0
  22. data/features/step_definitions/report_steps.rb +17 -0
  23. data/features/step_definitions/user_steps.rb +71 -0
  24. data/features/support/env.rb +65 -0
  25. data/features/user_report.feature +67 -0
  26. data/lib/bell/cli.rb +12 -0
  27. data/lib/bell/commands/command.rb +58 -0
  28. data/lib/bell/commands/contact_command.rb +91 -0
  29. data/lib/bell/commands/implosion_command.rb +14 -0
  30. data/lib/bell/commands/report_command.rb +69 -0
  31. data/lib/bell/commands/user_command.rb +74 -0
  32. data/lib/bell/commands.rb +5 -0
  33. data/lib/bell/csv_parser.rb +52 -0
  34. data/lib/bell/dispatcher.rb +13 -0
  35. data/lib/bell/displayable.rb +13 -0
  36. data/lib/bell/full_report.rb +145 -0
  37. data/lib/bell/handlers/contacts_handler.rb +79 -0
  38. data/lib/bell/handlers/implosions_handler.rb +9 -0
  39. data/lib/bell/handlers/reports_handler.rb +41 -0
  40. data/lib/bell/handlers/users_handler.rb +50 -0
  41. data/lib/bell/handlers.rb +4 -0
  42. data/lib/bell/message.rb +115 -0
  43. data/lib/bell/public_contact.rb +17 -0
  44. data/lib/bell/user.rb +16 -0
  45. data/lib/bell/user_contact.rb +18 -0
  46. data/lib/bell/user_report.rb +88 -0
  47. data/lib/bell/util.rb +31 -0
  48. data/lib/bell.rb +157 -0
  49. data/spec/bell/cli_spec.rb +4 -0
  50. data/spec/bell/commands/command_spec.rb +59 -0
  51. data/spec/bell/commands/contact_command_spec.rb +112 -0
  52. data/spec/bell/commands/implosion_command_spec.rb +31 -0
  53. data/spec/bell/commands/report_command_spec.rb +71 -0
  54. data/spec/bell/commands/user_command_spec.rb +128 -0
  55. data/spec/bell/csv_parser_spec.rb +167 -0
  56. data/spec/bell/dispatcher_spec.rb +4 -0
  57. data/spec/bell/full_report_spec.rb +4 -0
  58. data/spec/bell/handlers/contacts_handler_spec.rb +160 -0
  59. data/spec/bell/handlers/implosions_handler_spec.rb +11 -0
  60. data/spec/bell/handlers/reports_handler_spec.rb +183 -0
  61. data/spec/bell/handlers/users_handler_spec.rb +94 -0
  62. data/spec/bell/public_contact_spec.rb +4 -0
  63. data/spec/bell/user_contact_spec.rb +4 -0
  64. data/spec/bell/user_report_spec.rb +4 -0
  65. data/spec/bell/user_spec.rb +4 -0
  66. data/spec/spec_helper.rb +21 -0
  67. metadata +230 -0
@@ -0,0 +1,52 @@
1
+ module Bell
2
+ module CSVParser
3
+ extend self
4
+ include Displayable, Util::String
5
+
6
+ def parse_contacts(options)
7
+ contact_rows = CSV.read(options[:path])
8
+ rescue Errno::ENOENT
9
+ display(Message.no_such_file_or_directory(options[:path]))
10
+ rescue Errno::EISDIR
11
+ display(Message.path_is_a_directory(options[:path]))
12
+ rescue CSV::MalformedCSVError
13
+ display(Message.invalid_contacts_file(options[:path]))
14
+ else
15
+ user = User.find(:name => options[:user][:name]) if options[:user]
16
+ valid_contacts = []
17
+ contact_rows.each_with_index do |row, index|
18
+ if row.size < 2
19
+ display(Message.row_with_few_columns(row, index + 1))
20
+ elsif row.size > 2
21
+ display(Message.row_with_extra_columns(row, index + 1))
22
+ else
23
+ contact = if options[:user]
24
+ UserContact.new(:name => sanitize(row.first),
25
+ :number => row.last,
26
+ :user_id => user.id)
27
+ elsif options[:public]
28
+ PublicContact.new(:name => sanitize(row.first), :number => row.last)
29
+ end
30
+ if contact
31
+ if contact.valid?
32
+ valid_contacts << contact
33
+ else
34
+ display(Message.
35
+ formatted_contact_errors(contact,
36
+ :line_number => index + 1))
37
+ end
38
+ end
39
+ end
40
+ end
41
+ if contact_rows.size > 0
42
+ if contact_rows.size == valid_contacts.size
43
+ valid_contacts
44
+ else
45
+ raise InvalidContacts
46
+ end
47
+ else
48
+ nil
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ module Bell
2
+ class Dispatcher
3
+ include Util::String
4
+
5
+ def self.dispatch(command)
6
+ Object.
7
+ const_get(:Bell).
8
+ const_get(:Handlers).
9
+ const_get(camelize(command[:handler])).
10
+ send(command[:action], command[:params])
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Bell
2
+ module Displayable
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def display(text)
9
+ Bell.output.puts text.to_s
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,145 @@
1
+ # encoding: utf-8
2
+
3
+ module Bell
4
+ class FullReport
5
+ def initialize(phone_bill)
6
+ @phone_bill = phone_bill
7
+ end
8
+
9
+ def to_s
10
+ "[resumo]
11
+ #{formatted_total}
12
+ #{formatted_known_total}
13
+ #{formatted_public_total}
14
+ #{formatted_unknown_total}
15
+ #{formatted_fees_total}
16
+
17
+ [usuários]
18
+ #{users.map { |user| %{#{formatted_user(user)}} }.join("\n")}
19
+
20
+ [ligações públicas]
21
+ #{public_calls.map do |hash|
22
+ %{#{formatted_call(hash[:call], :contact => hash[:contact])}}
23
+ end.join("\n")}
24
+
25
+ [ligações desconhecidas]
26
+ #{unknown_calls.map do |call|
27
+ %{#{formatted_call(call)}}
28
+ end.join("\n")}".gsub(/^\ {7,}/, '')
29
+ end
30
+
31
+ private
32
+
33
+ def formatted_total
34
+ %{#{"Total:"}}.ljust(25) << sprintf("%.2f", total)
35
+ end
36
+
37
+ def formatted_known_total
38
+ %{#{"Usuários:"}}.ljust(25) << sprintf("%.2f", known_total)
39
+ end
40
+
41
+ def formatted_public_total
42
+ %{#{"Públicas:"}}.ljust(25) << sprintf("%.2f", public_total)
43
+ end
44
+
45
+ def formatted_unknown_total
46
+ %{#{"Desconhecidas:"}}.ljust(25) << sprintf("%.2f", unknown_total)
47
+ end
48
+
49
+ def formatted_fees_total
50
+ %{#{"Taxas:"}}.ljust(25) << sprintf("%.2f", fees_total)
51
+ end
52
+
53
+ def formatted_user(user)
54
+ %{#{user[:name]}}.ljust(25) << sprintf("%.2f", user[:total])
55
+ end
56
+
57
+ def formatted_call(call, options = {})
58
+ contact = call.number_called << if options[:contact]
59
+ " (#{options[:contact].name})"
60
+ else
61
+ ''
62
+ end
63
+ time = call.start_time.to_s
64
+ date = call.date[0, 8].to_s
65
+ cost = sprintf("%.2f", call.cost)
66
+ contact.ljust(30) << if time.empty?
67
+ date.empty? ? ' ' * 20 : ' ' * 10 << "#{date} "
68
+ else
69
+ date.empty? ? ' ' * 12 << "#{time}" : "#{time} #{date} "
70
+ end << cost
71
+ end
72
+
73
+ def calls
74
+ @calls ||= @phone_bill.calls
75
+ end
76
+
77
+ def fees
78
+ @fees ||= @phone_bill.fees
79
+ end
80
+
81
+ def known_calls
82
+ @known_calls ||= calls.inject([]) do |memo, call|
83
+ UserContact.find(:number => call.number_called) ? memo << call : memo
84
+ end
85
+ end
86
+
87
+ def public_calls
88
+ @public_calls ||= calls.inject([]) do |memo, call|
89
+ if contact = PublicContact.find(:number => call.number_called)
90
+ memo << { :call => call, :contact => contact }
91
+ else
92
+ memo
93
+ end
94
+ end
95
+ end
96
+
97
+ def unknown_calls
98
+ calls - (public_calls.map { |c| c[:call] } + known_calls)
99
+ end
100
+
101
+ def fees_total
102
+ @fees_total ||= fees.inject(0) { |memo, call| memo += call.cost.to_f }
103
+ end
104
+
105
+ def total
106
+ @total ||= @phone_bill.total
107
+ end
108
+
109
+ def known_total
110
+ @known_total ||= known_calls.inject(0) do |memo, call|
111
+ memo += call.cost.to_f
112
+ end
113
+ end
114
+
115
+ def public_total
116
+ @public_total ||= public_calls.inject(0) do |memo, hash|
117
+ memo += hash[:call].cost.to_f
118
+ end
119
+ end
120
+
121
+ def unknown_total
122
+ @unknown_total ||= unknown_calls.inject(0) do |memo, call|
123
+ memo += call.cost.to_f
124
+ end
125
+ end
126
+
127
+ def user_total(user_name)
128
+ user = User.find(:name => user_name)
129
+ subtotal = calls.inject(0) do |total, call|
130
+ if UserContact.find(:number => call.number_called, :user_id => user.id)
131
+ total += call.cost.to_f
132
+ else
133
+ total
134
+ end
135
+ end
136
+ subtotal + (unknown_total + fees_total) / User.all.size
137
+ end
138
+
139
+ def users
140
+ User.all.inject([]) do |users_hash, user|
141
+ users_hash << { :name => user.name, :total => user_total(user.name) }
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+
3
+ module Bell
4
+ module Handlers
5
+ class ContactsHandler
6
+ include Displayable
7
+
8
+ def self.list(params = {})
9
+ if params.empty?
10
+ if UserContact.empty?
11
+ display Message.no_contacts_created
12
+ else
13
+ display formatted_contact_list(UserContact.all + PublicContact.all)
14
+ end
15
+ else
16
+ if user = User.find(:name => params[:user][:name])
17
+ if user.contacts.empty?
18
+ display Message.contact_list_empty(user.name)
19
+ else
20
+ display formatted_contact_list(user.contacts,
21
+ :user_contacts => true,
22
+ :csv => params[:csv])
23
+ end
24
+ else
25
+ display Message.user_does_not_exist(params[:user][:name])
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.import(params)
31
+ if params[:user]
32
+ user = User.find(:name => params[:user][:name])
33
+ if user
34
+ if contacts = CSVParser.parse_contacts(params)
35
+ contacts.each do |contact|
36
+ contact.save && display(Message.contact_created(contact))
37
+ end
38
+ end
39
+ else
40
+ display(Message.user_does_not_exist(params[:user][:name]))
41
+ end
42
+ elsif params[:public]
43
+ if contacts = CSVParser.parse_contacts(params)
44
+ contacts.each do |contact|
45
+ contact.save &&
46
+ display(Message.contact_created(contact, :public => true))
47
+ end
48
+ end
49
+ end
50
+ rescue InvalidContacts
51
+ display(Message.no_contacts_created)
52
+ end
53
+
54
+ private
55
+
56
+ def self.text_contact_list(contacts, options)
57
+ contacts.inject('') do |list, contact|
58
+ list << "#{contact.name} (#{contact.number})"
59
+ if contact.is_a?(PublicContact)
60
+ list << " - público"
61
+ else
62
+ list << " - #{contact.user.name}" unless options[:user_contacts]
63
+ end
64
+ list << "\n"
65
+ end
66
+ end
67
+
68
+ def self.csv_contact_list(contacts)
69
+ contacts.inject('') do |list, contact|
70
+ list << "\"#{contact.name}\",#{contact.number}\n"
71
+ end
72
+ end
73
+
74
+ def self.formatted_contact_list(contacts, options = {})
75
+ options[:csv] ? csv_contact_list(contacts) : text_contact_list(contacts, options)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,9 @@
1
+ module Bell::Handlers
2
+ class ImplosionsHandler
3
+ include Bell::Displayable
4
+
5
+ def self.implode(params = {})
6
+ Bell.implode!
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ module Bell::Handlers
2
+ class ReportsHandler
3
+ include Bell::Displayable
4
+
5
+ def self.full_report(params = {})
6
+ phone_bill = Embratel::PhoneBill.new(params[:path])
7
+ rescue Errno::ENOENT
8
+ display Bell::Message.no_such_file_or_directory(params[:path])
9
+ rescue Errno::EISDIR
10
+ display Bell::Message.path_is_a_directory(params[:path])
11
+ rescue Bell::CSV::MalformedCSVError
12
+ display Bell::Message.malformed_csv_file(params[:path])
13
+ rescue Embratel::NonCSVFileError
14
+ display Bell::Message.non_csv_file(params[:path])
15
+ rescue Embratel::InvalidRowsError
16
+ display Bell::Message.invalid_rows(params[:path], $!.message)
17
+ else
18
+ display Bell::FullReport.new(phone_bill).to_s
19
+ end
20
+
21
+ def self.user_report(params = {})
22
+ phone_bill = Embratel::PhoneBill.new(params[:path])
23
+ rescue Errno::ENOENT
24
+ display Bell::Message.no_such_file_or_directory(params[:path])
25
+ rescue Errno::EISDIR
26
+ display Bell::Message.path_is_a_directory(params[:path])
27
+ rescue Bell::CSV::MalformedCSVError
28
+ display Bell::Message.malformed_csv_file(params[:path])
29
+ rescue Embratel::NonCSVFileError
30
+ display Bell::Message.non_csv_file(params[:path])
31
+ rescue Embratel::InvalidRowsError
32
+ display Bell::Message.invalid_rows(params[:path], $!.message)
33
+ else
34
+ if Bell::User.find(:name => params[:user][:name])
35
+ display Bell::UserReport.new(phone_bill, params[:user][:name]).to_s
36
+ else
37
+ display Bell::Message.user_does_not_exist(params[:user][:name])
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,50 @@
1
+ module Bell::Handlers
2
+ class UsersHandler
3
+ include Bell::Displayable
4
+
5
+ def self.create(params = {})
6
+ if Bell::User.find(:name => params[:user][:name])
7
+ display Bell::Message.user_already_exists(params[:user][:name])
8
+ else
9
+ Bell::User.create(:name => params[:user][:name])
10
+ display Bell::Message.user_created(params[:user][:name])
11
+ end
12
+ end
13
+
14
+ def self.rename(params = {})
15
+ if user = Bell::User.find(:name => params[:user][:source_name])
16
+ if Bell::User.find(:name => params[:user][:target_name])
17
+ display Bell::Message.user_already_exists(params[:user][:target_name])
18
+ else
19
+ user.name = params[:user][:target_name]
20
+ user.save
21
+ display Bell::Message.user_renamed(params[:user])
22
+ end
23
+ else
24
+ display Bell::Message.user_does_not_exist(params[:user][:source_name])
25
+ end
26
+ end
27
+
28
+ def self.list(params = {})
29
+ if Bell::User.empty?
30
+ display Bell::Message.no_created_users
31
+ else
32
+ display formatted_user_list
33
+ end
34
+ end
35
+
36
+ def self.remove(params = {})
37
+ if user = Bell::User.find(:name => params[:user][:name])
38
+ user.destroy
39
+ display Bell::Message.user_removed(params[:user][:name])
40
+ else
41
+ display Bell::Message.user_does_not_exist(params[:user][:name])
42
+ end
43
+ end
44
+
45
+ private
46
+ def self.formatted_user_list
47
+ Bell::User.all.map(&:name).join("\n")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,4 @@
1
+ require File.join(File.dirname(__FILE__), 'handlers', 'users_handler')
2
+ require File.join(File.dirname(__FILE__), 'handlers', 'contacts_handler')
3
+ require File.join(File.dirname(__FILE__), 'handlers', 'reports_handler')
4
+ require File.join(File.dirname(__FILE__), 'handlers', 'implosions_handler')
@@ -0,0 +1,115 @@
1
+ # encoding: utf-8
2
+
3
+ module Bell
4
+ module Message
5
+ extend self
6
+ include Util
7
+
8
+ def no_created_users
9
+ "Nenhum usuário criado."
10
+ end
11
+
12
+ def user_created(user_name)
13
+ "Usuário '#{user_name}' criado."
14
+ end
15
+
16
+ def user_already_exists(user_name)
17
+ "erro: o usuário '#{user_name}' já existe."
18
+ end
19
+
20
+ def user_does_not_exist(user_name)
21
+ "erro: o usuário '#{user_name}' não existe."
22
+ end
23
+
24
+ def user_removed(user_name)
25
+ "Usuário '#{user_name}' removido."
26
+ end
27
+
28
+ def user_renamed(user)
29
+ "Usuário '#{user[:source_name]}' renomeado para '#{user[:target_name]}'."
30
+ end
31
+
32
+ def no_contacts_created
33
+ "Nenhum contato criado."
34
+ end
35
+
36
+ def contact_list_empty(user_name)
37
+ "A lista de contatos do usuário '#{user_name}' está vazia."
38
+ end
39
+
40
+ def contact_created(contact, options = {})
41
+ "'#{contact.name} (#{contact.number})' adicionado à lista de " <<
42
+ if options[:public]
43
+ "contatos públicos."
44
+ else
45
+ "contatos do usuário '#{contact.user.name}'."
46
+ end
47
+ end
48
+
49
+ def contact_removed(contact_name)
50
+ "Contato '#{contact_name}' removido."
51
+ end
52
+
53
+ def contact_name_taken(contact_name)
54
+ contact = UserContact.find(:name => contact_name)
55
+ "erro: este nome já é usado pelo contato '#{contact.name} (#{contact.number})' do usuário '#{contact.user.name}'.\nCrie um contato com nome diferente." if contact
56
+ end
57
+
58
+ def contact_number_taken(contact_number)
59
+ contact = UserContact.find(:number => contact_number)
60
+ "erro: este número já é usado pelo contato '#{contact.name} (#{contact.number})' do usuário '#{contact.user.name}'.\nCrie um contato com número diferente." if contact
61
+ end
62
+
63
+ def contact_number_bad_format(contact_number)
64
+ "erro: '#{contact_number}' não é um número de telefone válido.\nVeja 'bell --help' para saber mais sobre o formato aceito."
65
+ end
66
+
67
+ def no_such_file_or_directory(path)
68
+ "erro: o arquivo/diretório '#{path}' não existe."
69
+ end
70
+
71
+ def path_is_a_directory(path)
72
+ "erro: '#{path}' é um diretório."
73
+ end
74
+
75
+ def malformed_csv_file(path)
76
+ "erro: '#{path}' é um arquivo CSV danificado."
77
+ end
78
+
79
+ def non_csv_file(path)
80
+ "erro: '#{path}' não é um arquivo CSV."
81
+ end
82
+
83
+ def invalid_rows(path, message)
84
+ lines = message.match(/(?:\d+, )*(?:\d+)$/).to_a
85
+ "erro: '#{path}' tem " <<
86
+ lines.size == 1 ? 'erro na linha ' : 'erros nas linhas ' <<
87
+ lines.join(', ')
88
+ end
89
+
90
+ def invalid_contacts_file(path)
91
+ "erro: '#{path}' não é um arquivo de contatos válido."
92
+ end
93
+
94
+ def row_with_extra_columns(row, line_number)
95
+ "erro: a linha #{line_number}: '#{row.to_s}' tem mais de duas colunas.\nCada linha deve ter duas colunas, sendo a primeira o nome e a segunda o telefone do contato."
96
+ end
97
+
98
+ def row_with_few_columns(row, line_number)
99
+ "erro: a linha #{line_number}: '#{row.to_s}' tem mais de duas colunas.\nCada linha deve ter duas colunas, sendo a primeira o nome e a segunda o telefone do contato."
100
+ end
101
+
102
+ def row_with_short_number(row, line_number)
103
+ "erro: o número de telefone #{row.to_s} na linha '#{line_number}' é muito curto.\nNúmeros de telefone devem ter 10 dígitos, sendo os 2 primeiros o DDD."
104
+ end
105
+
106
+ def formatted_contact_errors(contact, options = {})
107
+ contact_error = contact.errors.first.last.first
108
+ if options[:line_number]
109
+ contact_error.split("\n").first.insert(4, " na linha #{options[:line_number]}")
110
+ else
111
+ contact_error
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,17 @@
1
+ module Bell
2
+ class PublicContact < Sequel::Model
3
+ plugin :validation_helpers
4
+
5
+ def validate
6
+ super
7
+ validates_unique(:number,
8
+ :message => Message.contact_number_taken(number))
9
+ validates_format(/^\d{10}$/,
10
+ :number,
11
+ :message => Message.contact_number_bad_format(number))
12
+ if UserContact.find(:number => number)
13
+ errors.add(:number, Message.contact_number_taken(number))
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/bell/user.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Bell
2
+ class User < Sequel::Model
3
+ one_to_many(:contacts, :class => UserContact)
4
+ plugin(:validation_helpers)
5
+
6
+ def after_destroy
7
+ super
8
+ contacts.each(&:delete)
9
+ end
10
+
11
+ def validate
12
+ super
13
+ validates_unique(:name)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ module Bell
2
+ class UserContact < Sequel::Model
3
+ many_to_one(:user)
4
+ plugin(:validation_helpers)
5
+
6
+ def validate
7
+ super
8
+ validates_unique(:number,
9
+ :message => Message.contact_number_taken(number))
10
+ validates_format(/^\d{10}$/,
11
+ :number,
12
+ :message => Message.contact_number_bad_format(number))
13
+ if PublicContact.find(:number => number)
14
+ errors.add(:number, Message.contact_number_taken(number))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+
3
+ module Bell
4
+ class UserReport
5
+ include Bell::Util::String
6
+
7
+ def initialize(phone_bill, user_name)
8
+ @phone_bill = phone_bill
9
+ @user_name = user_name
10
+ end
11
+
12
+ def user
13
+ Bell::User.find(:name => @user_name)
14
+ end
15
+
16
+ def calls
17
+ @phone_bill.calls.inject([]) do |calls, call|
18
+ if Bell::UserContact.find(:user_id => user.id, :number => call.number_called)
19
+ calls << call
20
+ else
21
+ calls
22
+ end
23
+ end
24
+ end
25
+
26
+ def total
27
+ calls.inject(0) { |total, call| total += call.cost.to_f }
28
+ end
29
+
30
+ def to_s
31
+ "#{formatted_header}\n#{formatted_contact_calls}\n\n#{formatted_total}"
32
+ end
33
+
34
+ private
35
+
36
+ def formatted_header
37
+ "Data".ljust(15) <<
38
+ "Contato".ljust(20) <<
39
+ "Número".ljust(15) <<
40
+ "Horário".ljust(20) <<
41
+ "Custo"
42
+ end
43
+
44
+ def contact_calls
45
+ calls.inject([]) do |contact_calls, call|
46
+ if contact = Bell::UserContact.find(:number => call.number_called)
47
+ contact_calls << { :contact => contact, :call => call }
48
+ else
49
+ contact_calls
50
+ end
51
+ end
52
+ end
53
+
54
+ def formatted_total
55
+ "Total: R$ #{sprintf("%.2f", total)}"
56
+ end
57
+
58
+ def formatted_contact_name(contact_name)
59
+ if contact_name.size > 20
60
+ shortened_contact_name = contact_name[0, 15].rstrip.concat('...')
61
+ else
62
+ shortened_contact_name = contact_name
63
+ end
64
+
65
+ multibyte_characters = shortened_contact_name.size -
66
+ multibyte_length(shortened_contact_name)
67
+ formatted_contact_name = shortened_contact_name.
68
+ ljust(20 + multibyte_characters)
69
+ end
70
+
71
+ def formatted_contact_call(contact_call)
72
+ contact = contact_call[:contact]
73
+ call = contact_call[:call]
74
+
75
+
76
+ call.date[0, 8].ljust(15) <<
77
+ formatted_contact_name(contact.name) <<
78
+ call.number_called.ljust(15) <<
79
+ call.start_time.ljust(20) <<
80
+ sprintf("%.2f", call.cost)
81
+ end
82
+
83
+ def formatted_contact_calls
84
+ contact_calls.
85
+ map { |contact_call| formatted_contact_call(contact_call) }.join("\n")
86
+ end
87
+ end
88
+ end