Remove the trailing \r and trailing blanks from the description
[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    std::vector<BankTemplate> templates = read_templates("Bank.templ");
180
181    read_mutations(argv[optind], templates);
182 }
183
184 std::vector<BankTemplate> read_templates(const char * filename)
185 {
186    std::ifstream  in(filename);
187
188    String line;
189    BankTemplate   bt;
190    std::vector<BankTemplate>  templates;
191
192    bool           first = true;
193
194    while (in >> line)
195    {
196       if (~line > 0 && (line[0] == '-' || line[0] == '+'))
197       {
198          //  Start a new template
199
200          if (!first)
201          {
202             templates.push_back(bt);
203          }
204          first = false;
205
206          int separator = line.index('~');
207          bt = BankTemplate(line[0] == '+', line(1, separator -1), line(separator + 1, ~line - separator -1));
208       }
209       else
210       {
211          // Add to the template text
212          bt.add_to_template(line);
213       }
214    }
215
216    templates.push_back(bt);
217
218    return templates;
219 }
220
221 void read_mutations(const char *filename, std::vector<BankTemplate> templates)
222 {
223    int            i;
224
225    std::ifstream  in(filename);
226
227    String line;
228    std::vector<String> bank_record;
229
230    while (in >> line)
231    {
232       String description;
233
234       line += ",\"\"";
235       bank_record = parse_csv(line, ',');
236
237       description = bank_record[DESCRIP1];
238       for (i = DESCRIP2; i <= DESCRIP6; i++)
239       {
240          description += " " + bank_record[i];
241       }
242
243       //  Remove the trailing \r and trailing blanks from the description.
244
245       description >>= 1;
246       while (description[~description - 1] == ' ')
247       {
248          description >>= 1;
249       }
250
251       //  Find a template for this bank record
252
253       i = 0;
254       while (!templates[i].matches(bank_record[DEBIT_CREDIT], bank_record[NAME], description) && i < templates.size())
255       {
256          i++;
257       }
258       if (i != templates.size())
259       {
260          std::cout << templates[i].substitute_template(bank_record[BOOKING_DATE], bank_record[NAME],
261                                                        bank_record[AMOUNT], description);
262       }
263       else
264       {
265          std::cerr << "Bank record does not match any tempplate:\n";
266       }
267    }
268 }
269
270 std::vector<String> parse_csv(String line, char delim)
271 {
272    std::vector<String> record;
273    int i;
274    bool inquotes = false;
275    int  start = 0, len = 0;
276
277    for (i = 0; i < ~line; i++)
278    {
279       if (!inquotes && line[i] == '"')
280       {
281          inquotes = true;
282       }
283       else if (inquotes && line[i] == '"')
284       {
285          inquotes = false;
286       }
287       else if (!inquotes && line[i] == delim)
288       {
289          //  Delimiter: copy the substring to the record
290
291          String field = line(start, len);
292          record.push_back(field);
293          len = 0;
294       }
295       else
296       {
297          if (len == 0)
298          {
299             start = i;
300          }
301          len++;
302       }
303    }
304
305    return record;
306 }