total_in 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +9 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +22 -0
  8. data/README.md +21 -0
  9. data/Rakefile +1 -0
  10. data/lib/total_in/attribute_methods.rb +62 -0
  11. data/lib/total_in/contexts.rb +54 -0
  12. data/lib/total_in/document.rb +114 -0
  13. data/lib/total_in/line_handlers.rb +35 -0
  14. data/lib/total_in/line_parser.rb +42 -0
  15. data/lib/total_in/line_parsers.rb +80 -0
  16. data/lib/total_in/line_processors.rb +138 -0
  17. data/lib/total_in/parser.rb +48 -0
  18. data/lib/total_in/string_helpers.rb +10 -0
  19. data/lib/total_in/typecaster.rb +18 -0
  20. data/lib/total_in/version.rb +3 -0
  21. data/lib/total_in.rb +8 -0
  22. data/spec/fixtures/total_in_full.txt +61 -0
  23. data/spec/full_file_parse_spec.rb +501 -0
  24. data/spec/spec_helper.rb +20 -0
  25. data/spec/support/shared_examples_for_values_line_parsers.rb +15 -0
  26. data/spec/total_in/line_parsers/account_end_spec.rb +23 -0
  27. data/spec/total_in/line_parsers/account_start_spec.rb +24 -0
  28. data/spec/total_in/line_parsers/addresses_spec.rb +10 -0
  29. data/spec/total_in/line_parsers/deduction_start_spec.rb +50 -0
  30. data/spec/total_in/line_parsers/document_end_spec.rb +15 -0
  31. data/spec/total_in/line_parsers/document_start_spec.rb +32 -0
  32. data/spec/total_in/line_parsers/international_spec.rb +32 -0
  33. data/spec/total_in/line_parsers/locality_spec.rb +23 -0
  34. data/spec/total_in/line_parsers/messages_spec.rb +10 -0
  35. data/spec/total_in/line_parsers/names_spec.rb +11 -0
  36. data/spec/total_in/line_parsers/payment_start_spec.rb +45 -0
  37. data/spec/total_in/line_parsers/reference_numbers_spec.rb +11 -0
  38. data/spec/total_in/line_parsers/sender_account_spec.rb +23 -0
  39. data/spec/total_in/line_processors/account_end_spec.rb +35 -0
  40. data/spec/total_in/line_processors/account_start_spec.rb +50 -0
  41. data/spec/total_in/line_processors/addresses_spec.rb +44 -0
  42. data/spec/total_in/line_processors/deduction_start_spec.rb +54 -0
  43. data/spec/total_in/line_processors/document_end_spec.rb +32 -0
  44. data/spec/total_in/line_processors/document_start_spec.rb +29 -0
  45. data/spec/total_in/line_processors/international_spec.rb +46 -0
  46. data/spec/total_in/line_processors/locality_spec.rb +46 -0
  47. data/spec/total_in/line_processors/messages_spec.rb +25 -0
  48. data/spec/total_in/line_processors/names_spec.rb +45 -0
  49. data/spec/total_in/line_processors/payment_start_spec.rb +53 -0
  50. data/spec/total_in/line_processors/reference_numbers_spec.rb +24 -0
  51. data/spec/total_in/line_processors/sender_account_spec.rb +46 -0
  52. data/spec/total_in_spec.rb +13 -0
  53. data/total-in.gemspec +25 -0
  54. metadata +169 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 58ddedbe4d914ccb31dafda36de258488e2ad768
4
+ data.tar.gz: 1d9deb3b756e9c5f974c231d6992688c8b1f321d
5
+ SHA512:
6
+ metadata.gz: a4ed13121596580de19c2dc78abbd7ac60e4543986bb8132fdf0891a30335ed20600a7204b5c2f76dbeae0486a8970aff8c42a6e30853905879bccfadc4ec7b9
7
+ data.tar.gz: 57ac3813d93ac77374a1de9e7b48d2ec4a94b1e98a28cf37b417fcb87e8f98b7e34e84e8b43184e36120a34345a65949477b8cdc2d6ef5d6f0017208a8bb5a48
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ .DS_Store
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ bin/
19
+ tmp
20
+ bin
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --warnings
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.4
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.5
4
+ - 2.0.0-p598
5
+ script: rspec spec
6
+ addons:
7
+ code_climate:
8
+ repo_token:
9
+ secure: "MVPl+PV2Ewts0sumoKdjJxIX2A1jPJ80wXrFEEbeJMX3f3ST3PQtj2/XTRU4BBq1N2KzJL6ZUffpI9yKqSVsEHPhZan/ngixj5Z4pEEd59dWSJoC7ZxCWAq32qdoq2IAD2fRA7uvs34wo/D0BukTHqAFJXCfbpcoxskH/uzkLP8="
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "codeclimate-test-reporter", group: :test, require: nil
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Joel Junström
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # Total-IN file parser
2
+
3
+ [![Code Climate](https://codeclimate.com/github/Oktavilla/total-in-ruby/badges/gpa.svg)](https://codeclimate.com/github/Oktavilla/total-in-ruby)
4
+ [![Test Coverage](https://codeclimate.com/github/Oktavilla/total-in-ruby/badges/coverage.svg)](https://codeclimate.com/github/Oktavilla/total-in-ruby)
5
+
6
+
7
+ ## Usage
8
+
9
+ ```ruby
10
+ require "total_in"
11
+
12
+ result = TotalIn.parse total_in_file_contents
13
+ ```
14
+
15
+ ## Contributing
16
+
17
+ 1. Fork it ( http://github.com/oktavilla/total-in-ruby/fork )
18
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
19
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
20
+ 4. Push to the branch (`git push origin my-new-feature`)
21
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,62 @@
1
+ module AttributeMethods
2
+ def self.included base
3
+ base.send :include, InstanceMethods
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def attribute name
9
+ register_attribute name
10
+ define_attribute_reader name
11
+ define_attribute_writer name
12
+ end
13
+
14
+ def register_attribute name
15
+ self.attribute_names.push name
16
+ end
17
+
18
+ def attribute_names
19
+ @attribute_names ||= []
20
+ end
21
+
22
+ def define_attribute_reader name
23
+ define_method name do
24
+ read_attribute name
25
+ end
26
+ end
27
+
28
+ def define_attribute_writer name
29
+ define_method "#{name}=" do |value|
30
+ write_attribute name, value
31
+ end
32
+ end
33
+ end
34
+
35
+ module InstanceMethods
36
+ def initialize attrs = {}
37
+ self.assign_attributes attrs
38
+ end
39
+
40
+ def assign_attributes attrs
41
+ attrs.each do |name, value|
42
+ write_attribute name, value
43
+ end
44
+ end
45
+
46
+ def attributes
47
+ @attributes ||= {}
48
+ end
49
+
50
+ def read_attribute name
51
+ attributes[name]
52
+ end
53
+
54
+ def write_attribute name, value
55
+ attributes[name] = value if attribute_names.include?(name)
56
+ end
57
+
58
+ def attribute_names
59
+ self.class.attribute_names
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,54 @@
1
+ module TotalIn
2
+ class Contexts
3
+ def initialize containers = nil
4
+ Array(containers).compact.each do |container|
5
+ add container
6
+ end
7
+ end
8
+
9
+ def result
10
+ contexts.first
11
+ end
12
+
13
+ def current
14
+ contexts.last
15
+ end
16
+
17
+ def add container
18
+ contexts.push container
19
+ end
20
+
21
+ def move_up
22
+ contexts.pop
23
+ end
24
+
25
+ def move_to container_class
26
+ until current.is_a?(container_class)
27
+ move_up
28
+ end if contexts.any?
29
+ end
30
+
31
+ def move_to_or_add_to_parent container_class, parent_container_class
32
+ return self if current.is_a?(container_class)
33
+
34
+ until current.kind_of?(parent_container_class)
35
+ move_up
36
+ end
37
+
38
+ entity = container_class.new
39
+
40
+ setter_name = StringHelpers.underscore container_class.name.split("::").last
41
+ current.public_send "#{setter_name}=", entity
42
+
43
+ add entity
44
+
45
+ self
46
+ end
47
+
48
+ private
49
+
50
+ def contexts
51
+ @contexts ||= []
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,114 @@
1
+ require "total_in/string_helpers"
2
+ require "total_in/attribute_methods"
3
+
4
+ module TotalIn
5
+ class Document
6
+ include AttributeMethods
7
+ attribute :id
8
+ attribute :created_at
9
+ attribute :delivery_number
10
+ attribute :file_type
11
+ attribute :name
12
+ attribute :number_of_lines
13
+
14
+ def accounts
15
+ @accounts ||= []
16
+ end
17
+
18
+ class Account
19
+ include AttributeMethods
20
+ attribute :account_number
21
+ attribute :currency
22
+ attribute :date
23
+ attribute :number_of_transactions
24
+ attribute :amount
25
+ attribute :statement_reference
26
+
27
+ def payments
28
+ @payments ||= []
29
+ end
30
+
31
+ def deductions
32
+ @deductions ||= []
33
+ end
34
+ end
35
+
36
+ class Transaction
37
+ include AttributeMethods
38
+ def reference_numbers
39
+ @reference_numbers ||= []
40
+ end
41
+
42
+ attr_accessor :sender
43
+ attr_accessor :sender_account
44
+ attr_accessor :international
45
+
46
+ def message
47
+ messages.join "\n" if messages.any?
48
+ end
49
+
50
+ def messages
51
+ @messages ||= []
52
+ end
53
+
54
+ def add_message message
55
+ messages.push message
56
+ end
57
+
58
+ def international?
59
+ !!self.international
60
+ end
61
+ end
62
+
63
+ class Payment < Transaction
64
+ attribute :amount
65
+ attribute :serial_number
66
+ attribute :receiving_bankgiro_number
67
+ end
68
+
69
+ class Deduction < Transaction
70
+ attribute :amount
71
+ attribute :serial_number
72
+ attribute :receiving_bankgiro_number
73
+ attribute :code
74
+ end
75
+
76
+ class Sender
77
+ include AttributeMethods
78
+ attribute :name
79
+ attribute :address
80
+ attribute :postal_code
81
+ attribute :city
82
+ attribute :country_code
83
+
84
+ def self.add_to_contexts contexts
85
+ contexts.move_to_or_add_to_parent self, Transaction
86
+ end
87
+ end
88
+
89
+ class SenderAccount
90
+ include AttributeMethods
91
+ attribute :name
92
+ attribute :address
93
+ attribute :postal_code
94
+ attribute :city
95
+ attribute :country_code
96
+ attribute :account_number
97
+ attribute :origin_code
98
+ attribute :company_organization_number
99
+
100
+ def self.add_to_contexts contexts
101
+ contexts.move_to_or_add_to_parent self, Transaction
102
+ end
103
+ end
104
+
105
+ class International
106
+ include AttributeMethods
107
+ attribute :cost
108
+ attribute :cost_currency
109
+ attribute :amount
110
+ attribute :amount_currency
111
+ attribute :exchange_rate
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,35 @@
1
+ require "total_in/line_parsers"
2
+ require "total_in/line_processors"
3
+
4
+ module TotalIn
5
+ module LineHandlers
6
+ Handler = Struct.new :parser, :processor do
7
+ def handle line, contexts
8
+ line_parser = self.parser.new(line)
9
+
10
+ processor.call line_parser, contexts
11
+ end
12
+ end
13
+
14
+ def self.all
15
+ {
16
+ "00" => Handler.new(LineParsers::DocumentStart, LineProcessors::DocumentStart),
17
+ "99" => Handler.new(LineParsers::DocumentEnd, LineProcessors::DocumentEnd),
18
+ "10" => Handler.new(LineParsers::AccountStart, LineProcessors::AccountStart),
19
+ "90" => Handler.new(LineParsers::AccountEnd, LineProcessors::AccountEnd),
20
+ "20" => Handler.new(LineParsers::PaymentStart, LineProcessors::PaymentStart),
21
+ "25" => Handler.new(LineParsers::DeductionStart, LineProcessors::DeductionStart),
22
+ "30" => Handler.new(LineParsers::ReferenceNumbers, LineProcessors::ReferenceNumbers),
23
+ "40" => Handler.new(LineParsers::Messages, LineProcessors::Messages),
24
+ "50" => Handler.new(LineParsers::Names, LineProcessors::Names.new(Document::Sender)),
25
+ "51" => Handler.new(LineParsers::Addresses, LineProcessors::Addresses.new(Document::Sender)),
26
+ "52" => Handler.new(LineParsers::Locality, LineProcessors::Locality.new(Document::Sender)),
27
+ "60" => Handler.new(LineParsers::SenderAccount, LineProcessors::SenderAccount),
28
+ "61" => Handler.new(LineParsers::Names, LineProcessors::Names.new(Document::SenderAccount)),
29
+ "62" => Handler.new(LineParsers::Addresses, LineProcessors::Addresses.new(Document::SenderAccount)),
30
+ "63" => Handler.new(LineParsers::Locality, LineProcessors::Locality.new(Document::SenderAccount)),
31
+ "70" => Handler.new(LineParsers::International, LineProcessors::International)
32
+ }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ require "total_in/typecaster"
2
+
3
+ module TotalIn
4
+ class LineParser
5
+ attr_reader :line
6
+ def initialize line
7
+ @line = line
8
+ end
9
+
10
+ def attributes
11
+ self.class.field_names.each_with_object({}) do |field, attrs|
12
+ attrs[field] = self.public_send(field)
13
+ end
14
+ end
15
+
16
+ def self.field name, range, type = :string
17
+ self.field_names.push name
18
+
19
+ define_method name do
20
+ if range.is_a?(Array)
21
+ range.map { |r| value_at_position(r, type) }.compact
22
+ else
23
+ value_at_position range, type
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def self.field_names
31
+ @field_names ||= []
32
+ end
33
+
34
+ def value_at_position range, type
35
+ typecast line[range].strip, type
36
+ end
37
+
38
+ def typecast value, type
39
+ Typecaster.cast value, type
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,80 @@
1
+ require "total_in/line_parser"
2
+
3
+ module TotalIn
4
+ module LineParsers
5
+ class DocumentStart < LineParser
6
+ field :id, 2..13
7
+ field :created_at, 14..33, :time
8
+ field :delivery_number, 34..35, :integer
9
+ field :file_type, 36..38
10
+ field :name, 39..48
11
+ end
12
+
13
+ class DocumentEnd < LineParser
14
+ field :number_of_lines, 2..16, :integer
15
+ end
16
+
17
+ class AccountStart < LineParser
18
+ field :account_number, 2..37
19
+ field :currency, 38..40
20
+ field :date, 41..48, :date
21
+ end
22
+
23
+ class AccountEnd < LineParser
24
+ field :number_of_transactions, 2..9, :integer
25
+ field :amount, 10..26, :integer
26
+ field :statement_reference, 27..37
27
+ end
28
+
29
+ class PaymentStart < LineParser
30
+ field :reference_number, 2..36
31
+ field :amount, 37..51, :integer
32
+ field :serial_number, 52..68, :integer
33
+ field :receiving_bankgiro_number, 69..76
34
+ end
35
+
36
+ class DeductionStart < LineParser
37
+ field :reference_number, 2..36
38
+ field :amount, 37..51, :integer
39
+ field :serial_number, 52..68, :integer
40
+ field :code, 69..69, :integer
41
+ field :receiving_bankgiro_number, 70..77
42
+ end
43
+
44
+ class Values < LineParser
45
+ field :values, [2..36, 37..71]
46
+ end
47
+
48
+ class ReferenceNumbers < Values
49
+ end
50
+
51
+ class Messages < Values
52
+ end
53
+
54
+ class Names < Values
55
+ end
56
+
57
+ class Addresses < Values
58
+ end
59
+
60
+ class Locality < LineParser
61
+ field :postal_code, 2..10
62
+ field :city, 11..45
63
+ field :country_code, 46..47
64
+ end
65
+
66
+ class SenderAccount < LineParser
67
+ field :account_number, 2..37
68
+ field :origin_code, 38..38, :integer
69
+ field :company_organization_number, 39..58
70
+ end
71
+
72
+ class International < LineParser
73
+ field :cost, 2..16, :integer
74
+ field :cost_currency, 17..19
75
+ field :amount, 38..52, :integer
76
+ field :amount_currency, 53..55
77
+ field :exchange_rate, 56..67, :integer
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,138 @@
1
+ require "total_in/document"
2
+
3
+ module TotalIn
4
+ module LineProcessors
5
+ DocumentStart = ->(line, contexts) {
6
+ document = Document.new line.attributes
7
+
8
+ contexts.add document
9
+
10
+ contexts
11
+ }
12
+
13
+ DocumentEnd = ->(line, contexts) {
14
+ contexts.move_to Document
15
+
16
+ contexts.current.number_of_lines = line.number_of_lines
17
+
18
+ contexts
19
+ }
20
+
21
+ AccountStart = ->(line, contexts) {
22
+ contexts.move_to Document
23
+
24
+ account = Document::Account.new line.attributes
25
+
26
+ contexts.current.accounts << account
27
+ contexts.add account
28
+
29
+ contexts
30
+ }
31
+
32
+ AccountEnd = ->(line, contexts) {
33
+ contexts.move_to Document::Account
34
+
35
+ contexts.current.assign_attributes line.attributes
36
+
37
+ contexts
38
+ }
39
+
40
+ PaymentStart = ->(line, contexts) {
41
+ contexts.move_to Document::Account
42
+
43
+ payment = Document::Payment.new line.attributes
44
+ payment.reference_numbers << line.reference_number if line.reference_number
45
+
46
+ contexts.current.payments << payment
47
+
48
+ contexts.add payment
49
+
50
+ contexts
51
+ }
52
+
53
+ DeductionStart = ->(line, contexts) {
54
+ contexts.move_to Document::Account
55
+
56
+ deduction = Document::Deduction.new line.attributes
57
+ deduction.reference_numbers << line.reference_number if line.reference_number
58
+
59
+ contexts.current.deductions << deduction
60
+ contexts.add deduction
61
+
62
+ contexts
63
+ }
64
+
65
+ ReferenceNumbers = ->(line, contexts) {
66
+ contexts.current.reference_numbers.concat line.values
67
+
68
+ contexts
69
+ }
70
+
71
+ Messages = ->(line, contexts) {
72
+ line.values.each do |message|
73
+ contexts.current.add_message message
74
+ end
75
+
76
+ contexts
77
+ }
78
+
79
+ class WithTargetClass
80
+ def initialize target_class
81
+ @target_class = target_class
82
+ end
83
+
84
+ attr_reader :target_class
85
+ end
86
+
87
+ class Names < WithTargetClass
88
+ def call line, contexts
89
+ contexts = target_class.add_to_contexts contexts
90
+
91
+ contexts.current.name = line.values.join " "
92
+
93
+ contexts
94
+ end
95
+ end
96
+
97
+
98
+ class Addresses < WithTargetClass
99
+ def call line, contexts
100
+ contexts = target_class.add_to_contexts contexts
101
+
102
+ contexts.current.address = line.values.join " "
103
+
104
+ contexts
105
+ end
106
+ end
107
+
108
+ class Locality < WithTargetClass
109
+ def call line, contexts
110
+ contexts = target_class.add_to_contexts contexts
111
+
112
+ contexts.current.assign_attributes line.attributes
113
+
114
+ contexts
115
+ end
116
+ end
117
+
118
+ SenderAccount = ->(line, contexts) {
119
+ contexts = Document::SenderAccount.add_to_contexts contexts
120
+
121
+ contexts.current.assign_attributes line.attributes
122
+
123
+ contexts
124
+ }
125
+
126
+ International = ->(line, contexts) {
127
+ until contexts.current.kind_of?(Document::Transaction)
128
+ contexts.move_up
129
+ end
130
+
131
+ international = Document::International.new line.attributes
132
+
133
+ contexts.current.international = international
134
+
135
+ contexts
136
+ }
137
+ end
138
+ end
@@ -0,0 +1,48 @@
1
+ require "total_in/line_handlers"
2
+ require "total_in/contexts"
3
+
4
+ module TotalIn
5
+ class Parser
6
+ attr_reader :text
7
+
8
+ def initialize text
9
+ @text = text
10
+ end
11
+
12
+ def result
13
+ contexts = parse_lines text.each_line.to_a, Contexts.new
14
+
15
+ contexts.result
16
+ end
17
+
18
+ private
19
+
20
+ def parse_lines lines, contexts
21
+ if line = lines.shift
22
+ parse_lines lines, parse_line(line, contexts)
23
+ else
24
+ contexts
25
+ end
26
+ end
27
+
28
+ def parse_line line, contexts
29
+ handler = handler_for_line line
30
+
31
+ handler.handle line, contexts
32
+ end
33
+
34
+ def handler_for_line line
35
+ self.class.handlers.fetch line[0..1]
36
+ end
37
+
38
+ def self.handlers
39
+ @handlers ||= {}
40
+ end
41
+
42
+ def self.register_handlers handlers
43
+ @handlers = handlers
44
+ end
45
+
46
+ register_handlers LineHandlers.all
47
+ end
48
+ end
@@ -0,0 +1,10 @@
1
+ module TotalIn
2
+ module StringHelpers
3
+ def self.underscore word
4
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
5
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
6
+ word.tr!("-", "_")
7
+ word.downcase
8
+ end
9
+ end
10
+ end