csv2qif 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2009-09-28
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,27 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ bin/csv2qif
7
+ config/amex2008.yml
8
+ config/amexblue2008.yml
9
+ config/lufthansa.yml
10
+ config/schwabone.yml
11
+ config/website.yml
12
+ lib/csv2qif.rb
13
+ lib/csv2qif/cli.rb
14
+ lib/csv2qif/defaults.rb
15
+ lib/csv2qif/processor.rb
16
+ lib/csv2qif/qif.rb
17
+ lib/csv2qif/string_ext.rb
18
+ script/console
19
+ script/destroy
20
+ script/generate
21
+ script/txt2html
22
+ tasks/rspec.rake
23
+ website/index.html
24
+ website/index.txt
25
+ website/javascripts/rounded_corners_lite.inc.js
26
+ website/stylesheets/screen.css
27
+ website/template.html.erb
data/PostInstall.txt ADDED
@@ -0,0 +1,7 @@
1
+
2
+ For more information on csv2qif, see http://csv2qif.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
data/README.rdoc ADDED
@@ -0,0 +1,48 @@
1
+ = csv2qif
2
+
3
+ * http://github.com/#{github_username}/#{project_name}
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ * FIX (sudo gem install, anything else)
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2009 FIXME full name
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+ require './lib/csv2qif'
6
+
7
+ Hoe.plugin :newgem
8
+ Hoe.plugin :website
9
+ # Hoe.plugin :cucumberfeatures
10
+
11
+ # Generate all the Rake tasks
12
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
13
+ $hoe = Hoe.spec 'csv2qif' do
14
+ self.developer 'Heinrich Klobuczek', 'heinrich@mail.com'
15
+ #self.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
16
+ self.rubyforge_name = self.name # TODO this is default value
17
+ # self.extra_deps = [['activesupport','>= 2.0.2']]
18
+
19
+ end
20
+
21
+ require 'newgem/tasks'
22
+ Dir['tasks/**/*.rake'].each { |t| load t }
23
+
24
+ # TODO - want other tests/tasks run by default? Add them to the list
25
+ # remove_task :default
26
+ # task :default => [:spec, :features]
data/bin/csv2qif ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2009-9-28.
4
+ # Copyright (c) 2009. All rights reserved.
5
+
6
+ require 'rubygems'
7
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/csv2qif")
8
+
9
+ Csv2qif::CLI.execute(STDIN, STDOUT, ARGV)
@@ -0,0 +1,13 @@
1
+ type: CCard
2
+ header: 7
3
+ where: e
4
+ date: a
5
+ payee: c
6
+ category: m
7
+ amount: f
8
+ num: l
9
+ address: |
10
+ j ? k ? j+"\n"+k : j : k
11
+ mappings:
12
+ - /-/:/
13
+ - /^/Expenses:/
@@ -0,0 +1,13 @@
1
+ type: CCard
2
+ header: 7
3
+ where: e
4
+ date: a
5
+ payee: c
6
+ category: m
7
+ amount: f
8
+ num: l
9
+ address: |
10
+ j ? k ? j+"\n"+k : j : k
11
+ mappings:
12
+ - /-/:/
13
+ - /^/Private:Expenses:/
@@ -0,0 +1,10 @@
1
+ field_separator: ;
2
+ type: CCard
3
+ header: 2
4
+ date: d
5
+ payee: f
6
+ amount: |
7
+ n=='H'?m:-m.to_f
8
+ num: a
9
+ address: g
10
+ memo: h+"\n"+i
@@ -0,0 +1,7 @@
1
+ type: Bank
2
+ header: 2
3
+ where: |
4
+ d !='SWMXX' and not e =~ /FOR INV CKG OVERDRAFT \d+-\d+ type: IC OVERDRAFT TRF/
5
+ date: a
6
+ payee: e
7
+ amount: g
@@ -0,0 +1,2 @@
1
+ host: klobuczek@rubyforge.org
2
+ remote_dir: /var/www/gforge-projects/csv2qif
data/lib/csv2qif.rb ADDED
@@ -0,0 +1,8 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ Dir[File.join(File.dirname(__FILE__), 'csv2qif/**/*.rb')].sort.each { |lib| require lib }
5
+
6
+ module Csv2qif
7
+ VERSION = '0.0.1'
8
+ end
@@ -0,0 +1,129 @@
1
+ require 'optparse'
2
+
3
+ module Csv2qif
4
+ class CLI
5
+ DEFAULT_OPTIONS = {
6
+ :field_separator => ',',
7
+ :header => 1,
8
+ :date => "a",
9
+ :amount => "b",
10
+ :type => 'CCard'
11
+
12
+ }
13
+
14
+ def self.execute(stdin, stdout, arguments=[])
15
+
16
+ # NOTE: the option -p/--path= is given as an example, and should be replaced in your application.
17
+
18
+ options = {
19
+
20
+ }
21
+
22
+ mandatory_options = %w( )
23
+
24
+ parser = OptionParser.new do |opts|
25
+ opts.banner = <<-BANNER.gsub(/^ /, '')
26
+ cvs2qif -- format converter
27
+
28
+ Usage: #{File.basename($0)} [options] [file...]
29
+
30
+ The csv2qif utility reads the specified csv files, or the standard input if no files are specified,
31
+ converting the input to qif format. The output is written to either the standart output if read from
32
+ standard input or file with the same base name as input and qif extension or to a file specified as an
33
+ option.
34
+
35
+ The CONDITION and COLUMN below are in the simplest case represented by just one lower case letter (a-z) indicating the the csv column
36
+ which should be mapped to a particular line in a qif record. As a minimum a date and amount column should be mapped
37
+ e.g.
38
+ #> csv2qif -D a --amount b file.csv
39
+ will assume the first column (a) in the csv file is date and second (b) the amount. Those are as well defaults.
40
+ CONDITION and COLUMNS may as well be any ruby expression. In this case the expression will be evaluated in a context
41
+ in which the column names (a-z) are available as methods returning the value in the corresponding column or nil if empty.
42
+
43
+ Options are:
44
+ BANNER
45
+ opts.separator ""
46
+ opts.on("-b", "--bundle BUNDLE", String,
47
+ "Name of an option bundle",
48
+ "Default: default") { |arg| options[:bundle] = arg }
49
+ opts.on("-t", "--type Type", ['CCard', 'Bank', 'Cash'],
50
+ "Type of acoount: CCard, Bank or Cash",
51
+ "Default: CCard") { |arg| options[:type] = arg }
52
+ opts.on("-w", "--where CONDITION",
53
+ "only records satisfying CONDITION will be converted") { |arg| options[:where] = arg }
54
+ opts.on("-s", "--field_separator SEPARATOR",
55
+ "field seprator. Default: ,") { |arg| options[:where] = arg }
56
+ opts.on("-m", "--mappings MAPPINGS",
57
+ "comma separated list of mappings in the format: /pattern/replacement/",
58
+ "Use for modifying categories") { |arg| options[:mappings] = arg.split "," }
59
+ opts.on("-d", "--header N", Integer,
60
+ "number of rows occupied by headers before actual data") { |arg| options[:header] = arg }
61
+ opts.on("-h", "--help",
62
+ "Show this help message.") { stdout.puts opts; return }
63
+ opts.separator " "
64
+
65
+ opts.separator "QIF Record options:"
66
+ QIF::QIF_CODES.each do |key, code, description|
67
+ opts.on("-#{code}", "--#{key} COLUMN", String, description || key.to_s.capitalize) {|arg| options[key]=arg}
68
+ end
69
+
70
+ opts.parse!(arguments)
71
+
72
+ if mandatory_options && mandatory_options.find { |option| options[option.to_sym].nil? }
73
+ stdout.puts opts; exit
74
+ end
75
+ end
76
+
77
+ options = prepare_options stdout, options, Processor.init
78
+ prepare_mappings options
79
+
80
+ Processor.process stdin, stdout, arguments, options
81
+ end
82
+
83
+ private
84
+ def self.load_yml file, qif=nil
85
+ symbolize_keys YAML.load_file(file)
86
+ end
87
+
88
+ def self.load_rb file, qif
89
+ eval File.read(file), qif.block
90
+ end
91
+
92
+ def self.prepare_options stdout, options, qif
93
+ if h = options[:bundle] ? load_file(options[:bundle], qif) : {}
94
+ DEFAULT_OPTIONS.merge(h).merge options
95
+ else
96
+ stdout.puts("Specified bundle '#{options[:bundle]}' does not exist")
97
+ exit
98
+ end
99
+ end
100
+
101
+ def self.load_file bundle, qif=nil
102
+ ['.', File.join( File.dirname(__FILE__), "../../config")].each do |dir|
103
+ [:rb, :yml].each do |type|
104
+ if h = (File.exists?(file=File.join(dir, [bundle, type].join('.'))) and send "load_#{type}".to_sym, file, qif)
105
+ return h
106
+ end
107
+ end
108
+ end
109
+ nil
110
+ end
111
+
112
+ def self.symbolize_keys hash
113
+ hash.inject({}) do |options, (key, value)|
114
+ options[(key.to_sym rescue key) || key] = value
115
+ options
116
+ end
117
+ end
118
+
119
+ def self.prepare_mappings options
120
+ options[:mappings] = options[:mappings].map do |m|
121
+ m = m.split(m[0,1])[1,3] unless m.instance_of? Array
122
+ m.unshift true if m.length < 3
123
+ m[1]=Regexp.new(m[1])
124
+ m
125
+ end if options[:mappings]
126
+ end
127
+ end
128
+
129
+ end
@@ -0,0 +1,4 @@
1
+ COMMON_HEADINGS = {
2
+ :date => %w{datum},
3
+ :amount => %w{betrag, charge}
4
+ }
@@ -0,0 +1,43 @@
1
+ require 'csv'
2
+
3
+ class Processor
4
+
5
+ class << self
6
+ def init
7
+ @qif = QIF.new
8
+ end
9
+
10
+ def process stdin, stdout, arguments, options
11
+ if arguments.empty?
12
+ process_file stdin, stdout, options
13
+ else
14
+ arguments.each do |file|
15
+ stream_in = File.open(file, "r")
16
+ stream_out = File.new esub(file, :csv, :qif), "w"
17
+ begin
18
+ process_file stream_in, stream_out, options
19
+ ensure
20
+ stream_in.close
21
+ stream_out.close
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def esub file, old, new
30
+ file.gsub(/\.#{old}$/i, '')+".#{new}"
31
+ end
32
+
33
+ def process_file in_stream, out_stream, options
34
+ @qif.reset out_stream, options
35
+ rownum = 0
36
+ CSV::Reader.parse(in_stream, options[:field_separator]) do |row|
37
+ rownum += 1
38
+ @qif.header row if rownum == options[:header]
39
+ @qif.push row if rownum > options[:header]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,86 @@
1
+ class QIF
2
+ COLUMNS = ('a'..'z').to_a
3
+ QIF_CODES = [
4
+ [:date, :D],
5
+ [:amount, :T],
6
+ [:cleared, :C, 'Cleared Status'],
7
+ [:num, :N, 'Num (check or reference number)'],
8
+ [:payee, :P],
9
+ [:memo, :M],
10
+ [:address, :A, 'Address (up to five lines; the sixth line is an optional message)'],
11
+ [:category, :L, 'Category (Category/Subcategory/Transfer/Class)']
12
+ ]
13
+
14
+ def reset stream, options
15
+ @stream = stream
16
+ @options = options
17
+ #options.keys.each { |m| undef_method m }
18
+ stream.puts"!Type:#{options[:type]}"
19
+ end
20
+
21
+ def block
22
+ Proc.new {}
23
+ end
24
+
25
+ def header row
26
+
27
+ end
28
+
29
+ def push row
30
+ @row = row
31
+ return unless @options[:where].nil? or call_or_eval(:where)
32
+ QIF_CODES.each do |key, code|
33
+ next unless value = call_or_eval(key)
34
+ case key
35
+ when :date then
36
+ put_line code, (value.instance_of?(Date) ? value : Date.parse(value, true)).strftime("%m/%d/%Y")
37
+ when :amount then
38
+ put_line code, sprintf("%.2f", value.to_f)
39
+ when :address then
40
+ put_lines code, value, 5
41
+ when :category then
42
+ @options[:mappings].each {|condition, pattern, replacement| value.gsub!(pattern, replacement) if call_or_eval_or_value condition} if @options[:mappings]
43
+ put_line code, value
44
+ else
45
+ put_line code, value
46
+ end
47
+ end
48
+ @stream.puts '^'
49
+ end
50
+
51
+ def method_missing(sym, *args, &block)
52
+ @row[COLUMNS.index(sym.to_s)]
53
+ end
54
+
55
+ private
56
+
57
+ def call_or_eval key
58
+ if instr = @options[key]
59
+ call_or_eval_or_value instr
60
+ end
61
+ end
62
+
63
+ def call_or_eval_or_value value
64
+ value.instance_of?(Proc) ? value.call : value.instance_of?(String) ? eval(value) : value
65
+ end
66
+
67
+ def put_lines code, value, limit
68
+ overflow = []
69
+ value.each_line do |line|
70
+ unless (line = line.strip).empty?
71
+ if (limit -= 1) > 0
72
+ put_line code, line
73
+ else
74
+ overflow << line
75
+ end
76
+ end
77
+ end
78
+ put_line code, overflow.join(' ') unless overflow.empty?
79
+ end
80
+
81
+ def put_line code, value
82
+ return if (value = value.to_s.gsub(/\n+/, ' ').strip).empty?
83
+ @stream.puts code.to_s + value
84
+ end
85
+ end
86
+