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