Code cleanup
[account.git] / bank.cpp
1 /*
2  *  Read bank transactions and generate account bookings.
3  *
4  *  Template syntax:
5  *  Lines starting with '+' or '-': Match for name and description. Two regular
6  *       expressions, separated by '~'.  The '+' matches CREDIT transactions (adds to
7  *       the bank account); the '-' matches DEBIT transactions.
8  *
9  *  Other lines are content of the template. Parts enclosed in '~' characters are substituted by fields
10  *     from the input. Possibilities are: "$D" -> booking date, "$N" -> name, "$A" -> amount,
11  *     anything else is taken as a regular expression that is extracted from the description.
12  */
13
14
15 #include <fstream>
16 #include <vector>
17 #include <AXE/String.h>
18 #include <AXE/date.h>
19 #include <getopt.h>
20
21 class BankTemplate
22 {
23    bool Credit;
24    regex name_match;
25    regex description_match;
26
27    //  for debugging
28
29    String _nm;
30    String _dm;
31
32    String text_template;
33
34 public:
35
36    BankTemplate() : name_match(""), description_match(".*")
37    {
38       Credit = false;
39       text_template = "";
40    }
41
42    BankTemplate(bool cr, String nm, String dm)
43       : name_match(nm), description_match(dm)
44    {
45       Credit = cr;
46       text_template = "";
47       _nm = nm;
48       _dm = dm;
49    }
50
51    BankTemplate(const BankTemplate  & b)
52       : name_match(b.name_match), description_match(b.description_match)
53    {
54       Credit            = b.Credit;
55       text_template     = b.text_template;
56       _nm               = b._nm;
57       _dm               = b._dm;
58    }
59
60    void add_to_template(String s)
61    {
62       text_template += s + "\n";
63    }
64
65    bool matches(String deb_cred, String name, String descr);
66
67    String substitute_template(String date, String name, String amount, String descr);
68 };
69
70 bool BankTemplate::matches(String deb_cred, String name, String descr)
71 {
72    std::cout << name << " == " << _nm << "\n" << descr << " == " << _dm << "\n";
73    return (deb_cred == "C") == Credit && name == name_match && descr == description_match;
74 }
75
76 String BankTemplate::substitute_template(String date, String name, String amount, String descr)
77 {
78    String text(text_template);
79
80    int sub_start, sub_len;
81
82    sub_start = text.index('~');
83
84    while (sub_start != -1)
85    {
86       sub_len = 1;
87       while (text[sub_start + sub_len] != '~')
88       {
89          sub_len++;
90       }
91       sub_len++;
92       String match = text(sub_start, sub_len);
93       if (~match == 4 && match[1] == '$')
94       {
95          switch (match[2])
96          {
97          case 'A':
98             text(sub_start, sub_len) = amount;
99             break;
100
101          case 'N':
102             text(sub_start, sub_len) = name;
103             break;
104
105          case 'D':
106             text(sub_start, sub_len) = date(6,2) + String("-") + date(4,2) + String("-") + date(0,4);
107             break;
108
109          default:
110             text(sub_start, sub_len) = String("INVALID SUBSTITUTION");
111             break;
112          }
113       }
114       else
115       {
116          match <<= 1;
117          match >>= 1;
118          String extraction = descr(regex(match));
119          text(sub_start, sub_len) = extraction;
120       }
121
122       sub_start = text.index('~');
123    }
124
125    return text;
126 }
127
128 std::vector<BankTemplate> read_templates(const char * filename);
129 void read_mutations(const char *filename, std::vector<BankTemplate> templates);
130 std::vector<String> parse_csv(String line, char delim);
131
132 // Fields in the bank record.
133
134 const int DEBIT_CREDIT = 3;
135 const int AMOUNT       = 4;
136 const int NAME         = 6;
137 const int BOOKING_DATE = 7;
138 const int DESCRIP1     = 10;
139 const int DESCRIP2     = 11;
140 const int DESCRIP3     = 12;
141 const int DESCRIP4     = 13;
142 const int DESCRIP5     = 14;
143 const int DESCRIP6     = 15;
144
145 static char  helptext[] = 
146      "Usage: bank [options] filename\n"
147      "\n"
148      "Options:\n"
149      "  --help               :  Print this helpful message\n"
150      "  -V\n"
151      "  --version            :  Print the version number\n";
152
153 static struct option long_options[] =
154 {
155    { "help",       no_argument,        0,   'h' },
156    { "version",    no_argument,        0,   'V' },
157    { 0, 0, 0, 0 }
158 };
159
160 int main(int argc, char *argv[])
161 {
162    int      c;
163
164    int      option_index = 0;
165
166    while ((c = getopt_long(argc, argv, "hV", long_options, &option_index)) != -1)
167    {
168       switch (c)
169       {
170       case 'h':
171          std::cout << helptext;
172          exit(0);
173          break;
174
175       case 'V':
176          std::cout << "bank version 1.1\n";
177          exit(0);
178          break;
179
180       }
181    }
182
183    if (optind == argc)
184    {
185       std::cerr << "Usage: bank [options] filename\n";
186       exit(1);
187    }
188
189    std::vector<BankTemplate> templates = read_templates("Bank.templ");
190
191    read_mutations(argv[optind], templates);
192
193    return 0;
194 }
195
196 std::vector<BankTemplate> read_templates(const char * filename)
197 {
198    std::ifstream  in(filename);
199
200    String line;
201    BankTemplate   bt;
202    std::vector<BankTemplate>  templates;
203
204    bool           first = true;
205
206    while (in >> line)
207    {
208       if (~line > 0 && (line[0] == '-' || line[0] == '+'))
209       {
210          //  Start a new template
211
212          if (!first)
213          {
214             templates.push_back(bt);
215          }
216          first = false;
217
218          int separator = line.index('~');
219          bt = BankTemplate(line[0] == '+', line(1, separator -1), line(separator + 1, ~line - separator -1));
220       }
221       else
222       {
223          // Add to the template text
224          bt.add_to_template(line);
225       }
226    }
227
228    templates.push_back(bt);
229
230    return templates;
231 }
232
233 void read_mutations(const char *filename, std::vector<BankTemplate> templates)
234 {
235    unsigned int      i;
236
237    std::ifstream  in(filename);
238
239    String line;
240    std::vector<String> bank_record;
241
242    while (in >> line)
243    {
244       String description;
245
246       line += ",\"\"";
247       bank_record = parse_csv(line, ',');
248
249       description = bank_record[DESCRIP1];
250       for (i = DESCRIP2; i <= DESCRIP6; i++)
251       {
252          description += " " + bank_record[i];
253       }
254
255       //  Remove the trailing \r and trailing blanks from the description.
256
257       description >>= 1;
258       while (description[~description - 1] == ' ')
259       {
260          description >>= 1;
261       }
262
263       //  Find a template for this bank record
264
265       i = 0;
266       while (!templates[i].matches(bank_record[DEBIT_CREDIT], bank_record[NAME], description) && i < templates.size())
267       {
268          i++;
269       }
270       if (i != templates.size())
271       {
272          std::cout << templates[i].substitute_template(bank_record[BOOKING_DATE], bank_record[NAME],
273                                                        bank_record[AMOUNT], description);
274       }
275       else
276       {
277          std::cerr << "Bank record does not match any tempplate:\n";
278       }
279    }
280 }
281
282 std::vector<String> parse_csv(String line, char delim)
283 {
284    std::vector<String> record;
285    int i;
286    bool inquotes = false;
287    int  start = 0, len = 0;
288
289    for (i = 0; i < ~line; i++)
290    {
291       if (!inquotes && line[i] == '"')
292       {
293          inquotes = true;
294       }
295       else if (inquotes && line[i] == '"')
296       {
297          inquotes = false;
298       }
299       else if (!inquotes && line[i] == delim)
300       {
301          //  Delimiter: copy the substring to the record
302
303          String field = line(start, len);
304          record.push_back(field);
305          len = 0;
306       }
307       else
308       {
309          if (len == 0)
310          {
311             start = i;
312          }
313          len++;
314       }
315    }
316
317    return record;
318 }