PHP/JavaScript webapp to analyse spending habits
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

data.php 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <?PHP
  2. // Description prefixes used to indicate types
  3. $types = array(
  4. 'SO - ' => 'Standing Order',
  5. 'DD - Hbos Card Services' => 'Internal Transfer',
  6. 'DD - ' => 'Direct Debit',
  7. 'CHQ - ' => 'Cheque',
  8. 'Bank Credit - ' => 'Bank Credit',
  9. 'Bill Payment - Hfx Credit Card' => 'Internal Transfer',
  10. 'Bill Payment - ' => 'Bill Payment',
  11. 'DC - ' => 'Debit Card',
  12. 'DC Cashback - ' => 'Debit Card Cashback',
  13. 'DC Refund - ' => 'Debit Card Refund',
  14. 'Link ATM - ' => 'Cash Withdrawal',
  15. 'ATM - ' => 'Cash Withdrawal',
  16. 'Faster Payment - ' => 'Transfer',
  17. 'PAYMENT REC\'D - THANK YOU' => 'Internal Transfer',
  18. 'DIRECT DEBIT THANK YOU' => 'Internal Transfer',
  19. );
  20. // Types where the real description should be discarded in favour
  21. // of the type name
  22. $genericTypes = array(
  23. 'Internal Transfer',
  24. 'Cash Withdrawal',
  25. 'Cheque',
  26. );
  27. // Custom user rules for grouping different descriptions
  28. $rules = array(
  29. '^(?i)tesco' => 'Tesco',
  30. '^(?i)(sacat )?sainsbury\'?s' => 'Sainsbury\'s',
  31. '^(?i)marks & spencer' => 'M&S',
  32. '^(?i)argos' => 'Argos',
  33. '^(?i)subway' => 'Subway',
  34. '^(?i)specsavers' => 'Specsavers',
  35. '^(?i)adsl24' => 'ADSL 24',
  36. '^(?i)foxtons' => 'Foxtons',
  37. '^(?i)(eve online|ccp games)' => 'CCP Games',
  38. '^(?i)nandos' => 'Nandos',
  39. '^(?i)pizza express' => 'Pizza Express',
  40. '^(?i)steam(games|powered)\.com' => 'Steam',
  41. '^(?i)spotify(\.com|subs|\s)' => 'Spotify',
  42. '^(?i)t-\s?mobile' => 'T-Mobile',
  43. '^(?i)TGI Friday\'s' => 'TGI Friday\'s',
  44. '^(?i)wh smith' => 'WH Smiths',
  45. '^(?i)Codeweavers' => 'Codeweavers',
  46. '^(?i)Cineworld' => 'Cineworld',
  47. '^(?i)123-reg\.co\.uk' => '123-reg.co.uk',
  48. '866-321-8851' => 'Amazon Kindle',
  49. '(?i)Amazon Digital Dwnlds\s*amazon.co.uk' => 'Amazon MP3',
  50. '^(?i)Ocado' => 'Ocado',
  51. );
  52. // Categories
  53. $categories = array(
  54. 'Groceries' => array('Tesco', 'Ocado', 'M&S'),
  55. 'Home expenses' => array('T-Mobile', 'Bt Group Plc', 'Edf Energy', 'Foxtons', 'ADSL 24', 'Lb Southwark', '(?i)0800 Repair'),
  56. 'Entertainment' => array('Amazon', 'Spotify', 'Cineworld', '(?i)sky payments', 'Steam', '(?i)play.com'),
  57. 'Online' => array('(?i)giganews', 'Shane', '(?i)^github'),
  58. 'Cash' => array('Cash Withdrawal'),
  59. 'Going out' => array('(?i)tayyab', '(?i)founders arms', 'Nandos', '(?i)all bar one', 'TGI Friday\'s', '(?i)^piccolino', 'EAT & DRINK', '(?i)www.urbanbite.com'),
  60. 'Transport' => array('(?i)ec mainline'),
  61. 'Health/Medical' => array('Specsavers'),
  62. '(Ignored)' => array('Internal Transfer'),
  63. );
  64. // Formats part (one field) of a transaction
  65. function parseStatementPart($key, $value) {
  66. if ($key == 'Date') {
  67. $format = 'd/m/' . (strlen($value) == 8 ? 'y' : 'Y');
  68. return DateTime::createFromFormat($format, $value)->setTime(0, 0, 0);
  69. } else if ($key == 'Amount') {
  70. return (double) $value;
  71. }
  72. return $value;
  73. }
  74. // Formats an entire transaction from a statement
  75. function parseStatementLine($line) {
  76. global $categories, $genericTypes, $types, $rules;
  77. if (preg_match('/^(.*?)\s*\((.*? @ RATE .*?)\)$/', $line['Description'], $m)) {
  78. $line['Description'] = $m[1];
  79. $line['Exchange'] = $m[2];
  80. }
  81. foreach ($types as $prefix => $type) {
  82. if (strpos($line['Description'], $prefix) === 0) {
  83. $line['Type'] = $type;
  84. if (array_search($type, $genericTypes) === false) {
  85. $line['Description'] = substr($line['Description'], strlen($prefix));
  86. } else {
  87. $line['RawDescription'] = $line['Description'];
  88. $line['Description'] = $type;
  89. }
  90. break;
  91. }
  92. }
  93. foreach ($rules as $regex => $replacement) {
  94. if (preg_match('(' . $regex . ')', $line['Description'])) {
  95. $line['RawDescription'] = $line['Description'];
  96. $line['Description'] = $replacement;
  97. }
  98. }
  99. foreach ($categories as $cat => $entries) {
  100. foreach ($entries as $regex) {
  101. if (preg_match('(' . $regex . ')', $line['Description'])) {
  102. $line['Category'] = $cat;
  103. break;
  104. }
  105. }
  106. }
  107. return $line;
  108. }
  109. // Loads statements from the specified directory
  110. function loadStatements($dir = 'Statements') {
  111. $results = array();
  112. foreach (glob($dir . '/*.csv') as $statement) {
  113. $fh = fopen($statement, 'r');
  114. $data = array();
  115. $headers = array_map('trim', fgetcsv($fh));
  116. while (!feof($fh)) {
  117. $line = parseStatementLine(array_combine($headers, array_map('parseStatementPart', $headers, array_map('trim', fgetcsv($fh)))));
  118. $data[] = $line;
  119. }
  120. fclose($fh);
  121. $results[basename($statement)] = $data;
  122. }
  123. return $results;
  124. }
  125. $entries = array_reduce(loadStatements(), 'array_merge', array());
  126. usort($entries, function($a, $b) { return $a['Date']->getTimestamp() - $b['Date']->getTimestamp(); });
  127. $descs = array_unique(array_map(function($t) { return $t['Description']; }, $entries));
  128. sort($descs);
  129. $amounts = array();
  130. $rawmonths = array();
  131. $months = array();
  132. $bydesc = array();
  133. array_walk($entries, function($entry) use(&$months, &$bydesc, &$amounts, &$rawmonths) {
  134. $rawmonths[$entry['Date']->format('Y-m')][] = $entry;
  135. if (!isset($entry['Category']) || $entry['Category'] != '(Ignored)') {
  136. $amounts[$entry['Date']->format('Y-m')][$entry['Amount'] < 0 ? 'out' : 'in'] += $entry['Amount'];
  137. $months[$entry['Date']->format('Y-m')][$entry['Description']]['Count']++;
  138. $months[$entry['Date']->format('Y-m')][$entry['Description']]['Amount'] += $entry['Amount'];
  139. $bydesc[$entry['Description']]['Count']++;
  140. $bydesc[$entry['Description']]['Amount'] += $entry['Amount'];
  141. }
  142. });
  143. ksort($months);
  144. ksort($amounts);
  145. $monthsbydesc = array();
  146. array_walk(array_slice(array_reverse($months), 0, 6, true), function($monthentries, $month) use(&$monthsbydesc) {
  147. array_walk($monthentries, function($entry, $desc) use(&$monthsbydesc, $month) {
  148. $monthsbydesc[$desc][$month]['Count'] += $entry['Count'];
  149. $monthsbydesc[$desc][$month]['Amount'] += $entry['Amount'];
  150. });
  151. });
  152. $total = 0;
  153. array_walk($monthsbydesc, function($data, $desc) use(&$total) {
  154. $prob = count($data) / 6;
  155. $count = array_sum(array_map(function($x) { return $x['Count']; }, $data));
  156. $amount = array_sum(array_map(function($x) { return $x['Amount']; }, $data));
  157. $avgcount = $count / count($data);
  158. $avgamount = $amount / $count;
  159. $total += $prob * $avgcount * $avgamount;
  160. //echo "P($desc) = $prob, with avg of $avgcount trans/month, averaging $avgamount\n";
  161. });
  162. $transData = array(array(), array());
  163. array_walk($months, function($entries, $month) use(&$transData) {
  164. $ins = array_filter($entries, function($x) { return $x['Amount'] > 0; });
  165. $outs = array_filter($entries, function($x) { return $x['Amount'] < 0; });
  166. $totalin = array_sum(array_map(function($x) { return $x['Amount']; }, $ins));
  167. $totalout = array_sum(array_map(function($x) { return -1 * $x['Amount']; }, $outs));
  168. $time = strtotime($month . '-01') * 1000;
  169. $transData[0][] = array($time, $totalin);
  170. $transData[1][] = array($time, $totalout);
  171. });
  172. ?>
  173. var data = <?PHP echo json_encode($rawmonths); ?>;