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 5.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. <?PHP
  2. // Description prefixes used to indicate types
  3. // e.g. 'DD -' => 'Direct Debit'
  4. $types = array();
  5. // Types where the real description should be discarded in favour
  6. // of the type name
  7. // e.g. 'Cash Withdrawal'
  8. $genericTypes = array();
  9. // Custom user rules for grouping different descriptions
  10. // e.g. '(?i)^(company|service123)' => 'Company'
  11. $rules = array();
  12. // Categories
  13. // e.g. 'Groceries' => array('Shop1', 'Shop2', '(?i)regex')
  14. $categories = array();
  15. @include('data.local.php');
  16. if (!function_exists('parseStatementPart')) {
  17. // Formats part (one field) of a transaction
  18. function parseStatementPart($key, $value) {
  19. if ($key == 'Date') {
  20. $format = 'd/m/' . (strlen($value) == 8 ? 'y' : 'Y');
  21. return DateTime::createFromFormat($format, $value)->setTime(0, 0, 0);
  22. } else if ($key == 'Amount') {
  23. return (double) $value;
  24. }
  25. return $value;
  26. }
  27. }
  28. if (!function_exists('parseStatementLine')) {
  29. // Formats an entire transaction from a statement
  30. function parseStatementLine($line) {
  31. global $categories, $genericTypes, $types, $rules;
  32. if (!isset($line['Exchange']) || empty($line['Exchange'])) {
  33. if (preg_match('/^(.*?)\s*\((.*? @ RATE .*?)\)$/', $line['Description'], $m)) {
  34. $line['Description'] = $m[1];
  35. $line['Exchange'] = $m[2];
  36. }
  37. }
  38. if (!isset($line['Type']) || empty($line['Type'])) {
  39. foreach ($types as $prefix => $type) {
  40. if (strpos($line['Description'], $prefix) === 0) {
  41. $line['Type'] = $type;
  42. if (array_search($type, $genericTypes) === false) {
  43. $line['Description'] = substr($line['Description'], strlen($prefix));
  44. } else {
  45. $line['RawDescription'] = $line['Description'];
  46. $line['Description'] = $type;
  47. }
  48. break;
  49. }
  50. }
  51. }
  52. foreach ($rules as $regex => $replacement) {
  53. if (preg_match('(' . $regex . ')', $line['Description'])) {
  54. $line['RawDescription'] = $line['Description'];
  55. $line['Description'] = $replacement;
  56. }
  57. }
  58. if (!isset($line['Category']) || empty($line['Category'])) {
  59. foreach ($categories as $cat => $entries) {
  60. foreach ($entries as $regex) {
  61. if (preg_match('(' . $regex . ')', $line['Description'])) {
  62. $line['Category'] = $cat;
  63. break;
  64. }
  65. }
  66. }
  67. }
  68. return $line;
  69. }
  70. }
  71. if (!function_exists('loadStatements')) {
  72. // Loads statements from the specified directory
  73. function loadStatements($dir = 'Statements') {
  74. $results = array();
  75. foreach (glob($dir . '/*.csv') as $statement) {
  76. $fh = fopen($statement, 'r');
  77. $data = array();
  78. $headers = array_map('trim', fgetcsv($fh));
  79. while (!feof($fh)) {
  80. $line = parseStatementLine(array_combine($headers, array_map('parseStatementPart', $headers, array_map('trim', fgetcsv($fh)))));
  81. $data[] = $line;
  82. }
  83. fclose($fh);
  84. $results[basename($statement)] = $data;
  85. }
  86. return $results;
  87. }
  88. }
  89. $entries = array_reduce(loadStatements(), 'array_merge', array());
  90. usort($entries, function($a, $b) { return $a['Date']->getTimestamp() - $b['Date']->getTimestamp(); });
  91. $descs = array_unique(array_map(function($t) { return $t['Description']; }, $entries));
  92. sort($descs);
  93. $amounts = array();
  94. $rawmonths = array();
  95. $months = array();
  96. $bydesc = array();
  97. array_walk($entries, function($entry) use(&$months, &$bydesc, &$amounts, &$rawmonths) {
  98. $rawmonths[$entry['Date']->format('Y-m')][] = $entry;
  99. if (!isset($entry['Category']) || $entry['Category'] != '(Ignored)') {
  100. $amounts[$entry['Date']->format('Y-m')][$entry['Amount'] < 0 ? 'out' : 'in'] += $entry['Amount'];
  101. $months[$entry['Date']->format('Y-m')][$entry['Description']]['Count']++;
  102. $months[$entry['Date']->format('Y-m')][$entry['Description']]['Amount'] += $entry['Amount'];
  103. $bydesc[$entry['Description']]['Count']++;
  104. $bydesc[$entry['Description']]['Amount'] += $entry['Amount'];
  105. }
  106. });
  107. ksort($months);
  108. ksort($amounts);
  109. $monthsbydesc = array();
  110. array_walk(array_slice(array_reverse($months), 0, 6, true), function($monthentries, $month) use(&$monthsbydesc) {
  111. array_walk($monthentries, function($entry, $desc) use(&$monthsbydesc, $month) {
  112. $monthsbydesc[$desc][$month]['Count'] += $entry['Count'];
  113. $monthsbydesc[$desc][$month]['Amount'] += $entry['Amount'];
  114. });
  115. });
  116. $total = 0;
  117. array_walk($monthsbydesc, function($data, $desc) use(&$total) {
  118. $prob = count($data) / 6;
  119. $count = array_sum(array_map(function($x) { return $x['Count']; }, $data));
  120. $amount = array_sum(array_map(function($x) { return $x['Amount']; }, $data));
  121. $avgcount = $count / count($data);
  122. $avgamount = $amount / $count;
  123. $total += $prob * $avgcount * $avgamount;
  124. //echo "P($desc) = $prob, with avg of $avgcount trans/month, averaging $avgamount\n";
  125. });
  126. $transData = array(array(), array());
  127. array_walk($months, function($entries, $month) use(&$transData) {
  128. $ins = array_filter($entries, function($x) { return $x['Amount'] > 0; });
  129. $outs = array_filter($entries, function($x) { return $x['Amount'] < 0; });
  130. $totalin = array_sum(array_map(function($x) { return $x['Amount']; }, $ins));
  131. $totalout = array_sum(array_map(function($x) { return -1 * $x['Amount']; }, $outs));
  132. $time = strtotime($month . '-01') * 1000;
  133. $transData[0][] = array($time, $totalin);
  134. $transData[1][] = array($time, $totalout);
  135. });
  136. ?>
  137. var data = <?PHP echo json_encode($rawmonths); ?>;