Adapted to ACL
[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 <String.h>
18 #include <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;
107             text(sub_start, sub_len) = date(8,2) + String("-") + date(5,2) + String("-") + date(0,4);
108             break;
109
110          default:
111             text(sub_start, sub_len) = String("INVALID SUBSTITUTION");
112             break;
113          }
114       }
115       else
116       {
117          match <<= 1;
118          match >>= 1;
119          String extraction = descr(regex(match));
120          text(sub_start, sub_len) = extraction;
121       }
122
123       sub_start = text.index('~');
124    }
125
126    return text;
127 }
128
129 std::vector<BankTemplate> read_templates(const char * filename);
130 void read_mutations(const char *filename, std::vector<BankTemplate> templates);
131 std::vector<String> parse_csv(String line, char delim);
132
133 // Fields in the bank record.
134
135 const int DEBIT_CREDIT = 2;
136 const int AMOUNT       = 6;
137 const int NAME         = 9;
138 const int BOOKING_DATE = 4;
139 const int DESCRIP1     = 19;
140 const int DESCRIP2     = 20;
141 const int DESCRIP3     = 21;
142 const int DESCRIP4     = 13;
143 const int DESCRIP5     = 14;
144 const int DESCRIP6     = 15;
145
146 static char  helptext[] = 
147      "Usage: bank [options] filename\n"
148      "\n"
149      "Options:\n"
150      "  --help               :  Print this helpful message\n"
151      "  -V\n"
152      "  --version            :  Print the version number\n";
153
154 static struct option long_options[] =
155 {
156    { "help",       no_argument,        0,   'h' },
157    { "version",    no_argument,        0,   'V' },
158    { 0, 0, 0, 0 }
159 };
160
161 int main(int argc, char *argv[])
162 {
163    int      c;
164
165    int      option_index = 0;
166
167    while ((c = getopt_long(argc, argv, "hV", long_options, &option_index)) != -1)
168    {
169       switch (c)
170       {
171       case 'h':
172          std::cout << helptext;
173          exit(0);
174          break;
175
176       case 'V':
177          std::cout << "bank version 1.3\n";
178          exit(0);
179          break;
180
181       }
182    }
183
184    if (optind == argc)
185    {
186       std::cerr << "Usage: bank [options] filename\n";
187       exit(1);
188    }
189
190    std::vector<BankTemplate> templates = read_templates("Bank.templ");
191
192    read_mutations(argv[optind], templates);
193
194    return 0;
195 }
196
197 std::vector<BankTemplate> read_templates(const char * filename)
198 {
199    std::ifstream  in(filename);
200
201    String line;
202    BankTemplate   bt;
203    std::vector<BankTemplate>  templates;
204
205    bool           first = true;
206
207    while (in >> line)
208    {
209       if (~line > 0 && (line[0] == '-' || line[0] == '+'))
210       {
211          //  Start a new template
212
213          if (!first)
214          {
215             templates.push_back(bt);
216          }
217          first = false;
218
219          int separator = line.index('~');
220          bt = BankTemplate(line[0] == '+', line(1, separator -1), line(separator + 1, ~line - separator -1));
221       }
222       else
223       {
224          // Add to the template text
225          bt.add_to_template(line);
226       }
227    }
228
229    templates.push_back(bt);
230
231    return templates;
232 }
233
234 void read_mutations(const char *filename, std::vector<BankTemplate> templates)
235 {
236    unsigned int      i;
237
238    std::ifstream  in(filename);
239
240    String line;
241    std::vector<String> bank_record;
242
243    while (in >> line)
244    {
245       String description;
246
247       line += ",\"\"";
248       bank_record = parse_csv(line, ',');
249
250       // A + amount is Credit, a - amount is Debit
251
252       bank_record[DEBIT_CREDIT] = "C";
253       if (bank_record[AMOUNT][0] == '-')
254       {
255          bank_record[DEBIT_CREDIT] = "D";
256       }
257       bank_record[AMOUNT] <<= 1;
258
259       description = bank_record[DESCRIP1];
260       for (i = DESCRIP2; i <= DESCRIP3; i++)
261       {
262          description += " " + bank_record[i];
263       }
264
265       //  Remove the trailing \r and trailing blanks from the description.
266
267       description >>= 1;
268       while (description[~description - 1] == ' ')
269       {
270          description >>= 1;
271       }
272
273       //  Find a template for this bank record
274
275       i = 0;
276       while (!templates[i].matches(bank_record[DEBIT_CREDIT], bank_record[NAME], description) && i < templates.size())
277       {
278          i++;
279       }
280       if (i != templates.size())
281       {
282          std::cout << templates[i].substitute_template(bank_record[BOOKING_DATE], bank_record[NAME],
283                                                        bank_record[AMOUNT], description);
284       }
285       else
286       {
287          std::cerr << "Bank record does not match any tempplate:\n";
288       }
289    }
290 }
291
292 std::vector<String> parse_csv(String line, char delim)
293 {
294    std::vector<String> record;
295    int i;
296    bool inquotes = false;
297    int  start = 0, len = 0;
298
299    for (i = 0; i < ~line; i++)
300    {
301       if (!inquotes && line[i] == '"')
302       {
303          inquotes = true;
304       }
305       else if (inquotes && line[i] == '"')
306       {
307          inquotes = false;
308       }
309       else if (!inquotes && line[i] == delim)
310       {
311          //  Delimiter: copy the substring to the record
312
313          String field = line(start, len);
314          record.push_back(field);
315          len = 0;
316       }
317       else
318       {
319          if (len == 0)
320          {
321             start = i;
322          }
323          len++;
324       }
325    }
326
327    return record;
328 }