Add /categories-cvs

This commit is contained in:
Steve Schafer 2026-01-17 10:01:05 -07:00
parent 31f9272409
commit 3e978a831c
15 changed files with 365 additions and 43 deletions

View file

@ -2,7 +2,6 @@
<classpath> <classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
<attributes> <attributes>
<attribute name="module" value="true"/>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
@ -38,5 +37,22 @@
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/> <attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/> <classpathentry kind="output" path="target/classes"/>
</classpath> </classpath>

View file

@ -0,0 +1,2 @@
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=false

View file

@ -7,5 +7,6 @@ org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.compiler.source=17

View file

@ -1,13 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0"> <?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="budget.web-0.0.1-SNAPSHOT"> <wb-module deploy-name="budget.web-0.0.1-SNAPSHOT">
<wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/> <wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/>
<wb-resource deploy-path="/" source-path="/src/main/webapp"/> <wb-resource deploy-path="/" source-path="/src/main/webapp"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/> <wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/java"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="/target/generated-sources/annotations"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/resources"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="/target/generated-test-sources/test-annotations"/>
<property name="java-output-path" value="/com.stephenschafer.budget.web/build/classes"/> <property name="java-output-path" value="/com.stephenschafer.budget.web/build/classes"/>
<property name="context-root" value="budget.web"/> <property name="context-root" value="budget.web"/>
</wb-module> </wb-module>
</project-modules> </project-modules>

View file

@ -4,6 +4,6 @@
<fixed facet="java"/> <fixed facet="java"/>
<fixed facet="wst.jsdt.web"/> <fixed facet="wst.jsdt.web"/>
<installed facet="wst.jsdt.web" version="1.0"/> <installed facet="wst.jsdt.web" version="1.0"/>
<installed facet="jst.web" version="4.0"/>
<installed facet="java" version="17"/> <installed facet="java" version="17"/>
<installed facet="jst.web" version="2.5"/>
</faceted-project> </faceted-project>

View file

@ -20,4 +20,15 @@
<url-pattern>/categories</url-pattern> <url-pattern>/categories</url-pattern>
</servlet-mapping> </servlet-mapping>
<servlet>
<servlet-name>CategoriesCsvPage</servlet-name>
<servlet-class>com.stephenschafer.budget.web.CategoriesCsvPage</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CategoriesCsvPage</servlet-name>
<url-pattern>/categories-csv</url-pattern>
</servlet-mapping>
</web-app> </web-app>

View file

@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
if mvn -f pom.xml clean install > build-jar.log 2> build-jar.err.log; then if mvn -f pom.xml clean install > build-war.log 2> build-war.err.log; then
echo "success" echo "success"
cp target/*.war /tmp cp target/*.war /tmp
else else

View file

@ -0,0 +1,242 @@
package com.stephenschafer.budget.web;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import javax.naming.NamingException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CategoriesCsvPage extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void init(final ServletConfig config) throws ServletException {
try {
Configuration.INSTANCE.load();
}
catch (final Exception e) {
e.printStackTrace();
}
super.init(config);
}
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
var paramYear = request.getParameterValues("year");
if (paramYear == null || paramYear.length == 0) {
final Calendar cal = new GregorianCalendar();
cal.setTime(new Date());
final var year = cal.get(Calendar.YEAR);
paramYear = new String[] { String.valueOf(year) };
}
var tmpIncludeExpenses = true;
var tmpIncludeIncome = true;
var tmpIncludePayments = false;
var tmpIncludeInvestments = false;
final var includes = request.getParameterValues("include");
if (includes != null && includes.length > 0) {
tmpIncludeExpenses = false;
tmpIncludeIncome = false;
tmpIncludePayments = false;
tmpIncludeInvestments = false;
for (final String include : includes) {
if ("expenses".equalsIgnoreCase(include)) {
tmpIncludeExpenses = true;
}
else if ("income".equalsIgnoreCase(include)) {
tmpIncludeIncome = true;
}
else if ("payments".equalsIgnoreCase(include)) {
tmpIncludePayments = true;
}
else if ("investments".equalsIgnoreCase(include)) {
tmpIncludeInvestments = true;
}
}
}
final var includeExpenses = tmpIncludeExpenses;
final var includeIncome = tmpIncludeIncome;
final var includePayments = tmpIncludePayments;
final var includeInvestments = tmpIncludeInvestments;
final var startDates = request.getParameterValues("startDate");
final var endDates = request.getParameterValues("endDate");
final var startDateString = startDates == null ? null
: startDates.length == 0 ? null : startDates[0];
final var endDateString = endDates == null ? null
: endDates.length == 0 ? null : endDates[0];
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
final Date startDate;
final Date endDate;
try {
startDate = startDateString == null ? null : df.parse(startDateString);
endDate = endDateString == null ? null : df.parse(endDateString);
}
catch (final ParseException e) {
response.sendError(400, "Invalid date");
return;
}
final Calendar startCal = new GregorianCalendar();
startCal.setTime(startDate);
final int startMonth = startCal.get(Calendar.MONTH);
final Calendar endCal = new GregorianCalendar();
endCal.setTime(endDate);
final int endMonth = endCal.get(Calendar.MONTH);
final int monthCount = endMonth - startMonth + 1;
final var out = response.getWriter();
response.setHeader("Content-Type", "text/csv");
try (var connection = Configuration.INSTANCE.getConnection()) {
for (final String year : paramYear) {
final var categoryMap = loadCategories(connection, year, includeIncome,
includePayments, includeInvestments, includeExpenses);
final var sql = Util.getResourceAsString("getTransactionDetail.sql") //
.replace("${databaseName}", "budget_" + year) //
.replace("${regexDatabaseName}", "budget");
try (var stmt = connection.prepareStatement(sql)) {
try (var rs = stmt.executeQuery()) {
while (rs.next()) {
final var detail = new ReportDetail();
var i = 0;
detail.transactionId = rs.getInt(++i);
detail.date = rs.getDate(++i);
detail.source = rs.getString(++i);
detail.description = rs.getString(++i);
detail.amount = rs.getBigDecimal(++i);
if (rs.wasNull()) {
detail.amount = new BigDecimal(0);
}
final var categoryId = rs.getInt(++i);
detail.regex = rs.getString(++i);
detail.flags = rs.getInt(++i);
detail.requiredSource = rs.getString(++i);
detail.extraDescription = rs.getString(++i);
final var category = categoryMap.get(categoryId);
if (category != null && //
detail.isAfter(startDate) && //
detail.isBefore(endDate) && //
category.isIncluded()) {
category.addDetail(detail);
}
}
}
}
final var rootCategory = new ReportCategory(-1, null, "root");
for (final Integer categoryId : categoryMap.keySet()) {
final var category = categoryMap.get(categoryId);
if ((category != null) && (category.getParent() == null)) {
rootCategory.addChild(category);
}
}
out.print(Util.getResourceAsString("categoryCsvHead.template"));
generateCategoryTable(out, rootCategory, 0, monthCount);
}
}
catch (final SQLException | NamingException e) {
throw new ServletException(e);
}
}
private void generateCategoryTable(final PrintWriter out, final ReportCategory parent,
final int level, final int monthCount) throws IOException {
final var categoryGrandAverage = parent.getGrandAverage(monthCount);
final var categoryGrandAmount = parent.getGrandTotal();
var categoryCsvLine = Util.getResourceAsString("categoryCsv.template") //
.replace("${name}", parent.getQualifiedName()) //
.replace("${id}", parent.getId().toString()) //
.replace("${average}", categoryGrandAverage.toString()) //
.replace("${total}", categoryGrandAmount.toString()) //
.replace("${indent}", String.valueOf(level * 20) + "px");
parent.getLargestMonth();
for (int month = 0; month < 12; month++) {
categoryCsvLine = categoryCsvLine //
.replace("${total-" + month + "}", parent.getMonthGrandTotal(month).toString());
}
out.print(categoryCsvLine);
final var categories = parent.getChildCategories();
for (final ReportCategory category : categories) {
generateCategoryTable(out, category, level + 1, monthCount);
}
}
interface CategoryFilter {
boolean include(ReportCategory category);
}
private Map<Integer, ReportCategory> loadCategories(final Connection connection, final String year,
final boolean includeIncome, final boolean includePayments,
final boolean includeInvestments, final boolean includeExpenses)
throws SQLException, IOException {
ReportCategory unknownCategory = null;
final Map<Integer, ReportCategory> categories = new HashMap<>();
final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}",
"budget");
try (var stmt = connection.prepareStatement(sql)) {
try (var rs = stmt.executeQuery()) {
while (rs.next()) {
final Integer id = rs.getInt(1);
Integer parentId = rs.getInt(2);
if (rs.wasNull()) {
parentId = null;
}
final var name = rs.getString(3);
final var category = new ReportCategory(id, parentId, name);
categories.put(id, category);
if ("unknown".equals(name) && parentId == null) {
unknownCategory = category;
}
}
}
}
for (final Integer categoryId : categories.keySet()) {
final var category = categories.get(categoryId);
category.updateParent(categories);
}
if (unknownCategory == null) {
unknownCategory = new ReportCategory(-1, null, "unknown");
categories.put(unknownCategory.getId(), unknownCategory);
}
for (final Integer categoryId : categories.keySet()) {
final var category = categories.get(categoryId);
final CategoryFilter filter = category1 -> {
final String categoryName = category1.getName();
if ("income".equals(categoryName) || "refunds".equals(categoryName)) {
if (!includeIncome) {
return false;
}
}
else if ("payment".equals(categoryName)) {
if (!includePayments) {
return false;
}
}
else if ("investment".equals(categoryName)) {
if (!includeInvestments) {
return false;
}
}
else if (!includeExpenses) {
return false;
}
return true;
};
final boolean included = filter.include(category);
category.setIncluded(included);
}
return categories;
}
}

View file

@ -2,6 +2,7 @@ package com.stephenschafer.budget.web;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.math.BigDecimal;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.text.DateFormat; import java.text.DateFormat;
@ -94,6 +95,13 @@ public class CategoriesPage extends HttpServlet {
response.sendError(400, "Invalid date"); response.sendError(400, "Invalid date");
return; return;
} }
final Calendar startCal = new GregorianCalendar();
startCal.setTime(startDate);
final int startMonth = startCal.get(Calendar.MONTH);
final Calendar endCal = new GregorianCalendar();
endCal.setTime(endDate);
final int endMonth = endCal.get(Calendar.MONTH);
final int monthCount = endMonth - startMonth + 1;
final var out = response.getWriter(); final var out = response.getWriter();
response.setHeader("Content-Type", "text/html"); response.setHeader("Content-Type", "text/html");
try (var connection = Configuration.INSTANCE.getConnection()) { try (var connection = Configuration.INSTANCE.getConnection()) {
@ -118,13 +126,16 @@ public class CategoriesPage extends HttpServlet {
try (var stmt = connection.prepareStatement(sql)) { try (var stmt = connection.prepareStatement(sql)) {
try (var rs = stmt.executeQuery()) { try (var rs = stmt.executeQuery()) {
while (rs.next()) { while (rs.next()) {
final var detail = new Detail(); final var detail = new ReportDetail();
var i = 0; var i = 0;
detail.transactionId = rs.getInt(++i); detail.transactionId = rs.getInt(++i);
detail.date = rs.getDate(++i); detail.date = rs.getDate(++i);
detail.source = rs.getString(++i); detail.source = rs.getString(++i);
detail.description = rs.getString(++i); detail.description = rs.getString(++i);
detail.amount = rs.getBigDecimal(++i); detail.amount = rs.getBigDecimal(++i);
if (rs.wasNull()) {
detail.amount = new BigDecimal(0);
}
final var categoryId = rs.getInt(++i); final var categoryId = rs.getInt(++i);
detail.regex = rs.getString(++i); detail.regex = rs.getString(++i);
detail.flags = rs.getInt(++i); detail.flags = rs.getInt(++i);
@ -140,7 +151,7 @@ public class CategoriesPage extends HttpServlet {
} }
} }
} }
final var rootCategory = new Category(-1, null, "root"); final var rootCategory = new ReportCategory(-1, null, "root");
for (final Integer categoryId : categoryMap.keySet()) { for (final Integer categoryId : categoryMap.keySet()) {
final var category = categoryMap.get(categoryId); final var category = categoryMap.get(categoryId);
if ((category != null) && (category.getParent() == null)) { if ((category != null) && (category.getParent() == null)) {
@ -155,7 +166,7 @@ public class CategoriesPage extends HttpServlet {
out.println(grandHead); out.println(grandHead);
out.println("</thead>"); out.println("</thead>");
out.println("<tbody>"); out.println("<tbody>");
generateCategoryTable(out, rootCategory, 0); generateCategoryTable(out, rootCategory, 0, monthCount);
out.println("</tbody>"); out.println("</tbody>");
out.println("</table>"); out.println("</table>");
out.println("</div>"); out.println("</div>");
@ -168,15 +179,15 @@ public class CategoriesPage extends HttpServlet {
} }
} }
private void generateCategoryTable(final PrintWriter out, final Category parent, private void generateCategoryTable(final PrintWriter out, final ReportCategory parent,
final int level) throws IOException { final int level, final int monthCount) throws IOException {
final var details = parent.getDetails(); final var details = parent.getDetails();
final var categoryAmount = parent.getDetailTotal(); final var categoryGrandAverage = parent.getGrandAverage(monthCount);
final var categoryGrandAmount = parent.getGrandTotal(); final var categoryGrandAmount = parent.getGrandTotal();
var categoryHeadHtml = Util.getResourceAsString("categoryHead.html") // var categoryHeadHtml = Util.getResourceAsString("categoryHead.html") //
.replace("${name}", parent.getName()) // .replace("${name}", parent.getName()) //
.replace("${id}", parent.getId().toString()) // .replace("${id}", parent.getId().toString()) //
.replace("${amount}", categoryAmount.toString()) // .replace("${average}", categoryGrandAverage.toString()) //
.replace("${total}", categoryGrandAmount.toString()) // .replace("${total}", categoryGrandAmount.toString()) //
.replace("${indent}", String.valueOf(level * 20) + "px"); .replace("${indent}", String.valueOf(level * 20) + "px");
final int largestMonth = parent.getLargestMonth(); final int largestMonth = parent.getLargestMonth();
@ -195,7 +206,7 @@ public class CategoriesPage extends HttpServlet {
out.print(categoryDetailHeadHtml); out.print(categoryDetailHeadHtml);
} }
Collections.sort(details); Collections.sort(details);
for (final Detail detail : details) { for (final ReportDetail detail : details) {
final var categoryDetailHtml = Util.getResourceAsString("categoryDetail.html") // final var categoryDetailHtml = Util.getResourceAsString("categoryDetail.html") //
.replace("${source}", detail.source) // .replace("${source}", detail.source) //
.replace("${description}", detail.description) // .replace("${description}", detail.description) //
@ -214,8 +225,8 @@ public class CategoriesPage extends HttpServlet {
out.println("</td>"); out.println("</td>");
out.println("</tr>"); out.println("</tr>");
final var categories = parent.getChildCategories(); final var categories = parent.getChildCategories();
for (final Category category : categories) { for (final ReportCategory category : categories) {
generateCategoryTable(out, category, level + 1); generateCategoryTable(out, category, level + 1, monthCount);
} }
} }
@ -252,15 +263,15 @@ public class CategoriesPage extends HttpServlet {
} }
interface CategoryFilter { interface CategoryFilter {
boolean include(Category category); boolean include(ReportCategory category);
} }
private Map<Integer, Category> loadCategories(final Connection connection, final String year, private Map<Integer, ReportCategory> loadCategories(final Connection connection, final String year,
final boolean includeIncome, final boolean includePayments, final boolean includeIncome, final boolean includePayments,
final boolean includeInvestments, final boolean includeExpenses) final boolean includeInvestments, final boolean includeExpenses)
throws SQLException, IOException { throws SQLException, IOException {
Category unknownCategory = null; ReportCategory unknownCategory = null;
final Map<Integer, Category> categories = new HashMap<>(); final Map<Integer, ReportCategory> categories = new HashMap<>();
final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}", final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}",
"budget"); "budget");
try (var stmt = connection.prepareStatement(sql)) { try (var stmt = connection.prepareStatement(sql)) {
@ -272,7 +283,7 @@ public class CategoriesPage extends HttpServlet {
parentId = null; parentId = null;
} }
final var name = rs.getString(3); final var name = rs.getString(3);
final var category = new Category(id, parentId, name); final var category = new ReportCategory(id, parentId, name);
categories.put(id, category); categories.put(id, category);
if ("unknown".equals(name) && parentId == null) { if ("unknown".equals(name) && parentId == null) {
unknownCategory = category; unknownCategory = category;
@ -285,7 +296,7 @@ public class CategoriesPage extends HttpServlet {
category.updateParent(categories); category.updateParent(categories);
} }
if (unknownCategory == null) { if (unknownCategory == null) {
unknownCategory = new Category(-1, null, "unknown"); unknownCategory = new ReportCategory(-1, null, "unknown");
categories.put(unknownCategory.getId(), unknownCategory); categories.put(unknownCategory.getId(), unknownCategory);
} }
for (final Integer categoryId : categories.keySet()) { for (final Integer categoryId : categories.keySet()) {

View file

@ -1,29 +1,33 @@
package com.stephenschafer.budget.web; package com.stephenschafer.budget.web;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
public class Category { public class ReportCategory {
private final Integer id; private final Integer id;
private final Integer parentId; private final Integer parentId;
private final String name; private final String name;
private Category parent; private ReportCategory parent;
private boolean included; private boolean included;
private List<Detail> details = new ArrayList<>(); private List<ReportDetail> details = new ArrayList<>();
private final Map<Integer, Category> children = new HashMap<>(); private Set<Integer> months = null;
private final Map<Integer, ReportCategory> children = new HashMap<>();
public Category(final Integer id, final Integer parentId, final String name) { public ReportCategory(final Integer id, final Integer parentId, final String name) {
this.id = id; this.id = id;
this.parentId = parentId; this.parentId = parentId;
this.name = name; this.name = name;
} }
public void updateParent(final Map<Integer, Category> categories) { public void updateParent(final Map<Integer, ReportCategory> categories) {
if (parentId == null) { if (parentId == null) {
parent = null; parent = null;
} }
@ -33,15 +37,15 @@ public class Category {
} }
} }
public void addChild(final Category category) { public void addChild(final ReportCategory category) {
children.put(category.id, category); children.put(category.id, category);
} }
public Category getParent() { public ReportCategory getParent() {
return parent; return parent;
} }
public void setParent(final Category parent) { public void setParent(final ReportCategory parent) {
this.parent = parent; this.parent = parent;
} }
@ -67,25 +71,25 @@ public class Category {
return sb.toString(); return sb.toString();
} }
public List<Detail> getDetails() { public List<ReportDetail> getDetails() {
return details; return details;
} }
public void setDetails(final List<Detail> details) { public void setDetails(final List<ReportDetail> details) {
this.details = details; this.details = details;
} }
public void addDetail(final Detail detail) { public void addDetail(final ReportDetail detail) {
details.add(detail); details.add(detail);
} }
public Map<Integer, Category> getChildren() { public Map<Integer, ReportCategory> getChildren() {
return children; return children;
} }
public BigDecimal getDetailTotal() { public BigDecimal getDetailTotal() {
var amount = new BigDecimal(0); var amount = new BigDecimal(0);
for (final Detail detail : details) { for (final ReportDetail detail : details) {
amount = amount.add(detail.amount); amount = amount.add(detail.amount);
} }
return amount; return amount;
@ -93,7 +97,7 @@ public class Category {
public BigDecimal getMonthTotal(final int month) { public BigDecimal getMonthTotal(final int month) {
var amount = new BigDecimal(0); var amount = new BigDecimal(0);
for (final Detail detail : details) { for (final ReportDetail detail : details) {
final Calendar cal = new GregorianCalendar(); final Calendar cal = new GregorianCalendar();
cal.setTime(detail.date); cal.setTime(detail.date);
if (month == cal.get(Calendar.MONTH)) { if (month == cal.get(Calendar.MONTH)) {
@ -103,6 +107,21 @@ public class Category {
return amount; return amount;
} }
public Set<Integer> getMonths() {
Set<Integer> months = this.months;
if (months == null) {
months = new HashSet<>();
for (final ReportDetail detail : details) {
final Calendar cal = new GregorianCalendar();
cal.setTime(detail.date);
final int month = cal.get(Calendar.MONTH);
months.add(Integer.valueOf(month));
}
this.months = months;
}
return months;
}
public BigDecimal getGrandTotal() { public BigDecimal getGrandTotal() {
var total = getDetailTotal(); var total = getDetailTotal();
for (final Integer categoryId : children.keySet()) { for (final Integer categoryId : children.keySet()) {
@ -112,6 +131,14 @@ public class Category {
return total; return total;
} }
public BigDecimal getGrandAverage(final int monthCount) {
final BigDecimal grandTotal = getGrandTotal();
if (monthCount == 0) {
return new BigDecimal(0);
}
return grandTotal.divide(new BigDecimal(monthCount), RoundingMode.HALF_DOWN);
}
public BigDecimal getMonthGrandTotal(final int month) { public BigDecimal getMonthGrandTotal(final int month) {
var total = getMonthTotal(month); var total = getMonthTotal(month);
for (final Integer categoryId : children.keySet()) { for (final Integer categoryId : children.keySet()) {
@ -139,8 +166,8 @@ public class Category {
return maxCount > 1 ? -1 : maxMonth; return maxCount > 1 ? -1 : maxMonth;
} }
public List<Category> getChildCategories() { public List<ReportCategory> getChildCategories() {
final List<Category> categories = new ArrayList<>(); final List<ReportCategory> categories = new ArrayList<>();
for (final Integer categoryId : children.keySet()) { for (final Integer categoryId : children.keySet()) {
final var category = children.get(categoryId); final var category = children.get(categoryId);
categories.add(category); categories.add(category);

View file

@ -3,7 +3,7 @@ package com.stephenschafer.budget.web;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
class Detail implements Comparable<Detail> { class ReportDetail implements Comparable<ReportDetail> {
int transactionId; int transactionId;
String source; String source;
String description; String description;
@ -15,7 +15,7 @@ class Detail implements Comparable<Detail> {
String extraDescription; String extraDescription;
@Override @Override
public int compareTo(final Detail arg1) { public int compareTo(final ReportDetail arg1) {
final var arg0 = this; final var arg0 = this;
int comparison; int comparison;
if (arg0.date == null) { if (arg0.date == null) {

View file

@ -0,0 +1 @@
${name},${average}, ${total}, ${total-0}, ${total-1}, ${total-2}, ${total-3}, ${total-4}, ${total-5}, ${total-6}, ${total-7}, ${total-8}, ${total-9}, ${total-10}, ${total-11}

View file

@ -0,0 +1 @@
Category, Average, Total, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec

View file

@ -3,7 +3,7 @@
<h2 style="cursor: pointer; font-weight: normal;" onclick="showDetail(this, ${id})">${name}</h2> <h2 style="cursor: pointer; font-weight: normal;" onclick="showDetail(this, ${id})">${name}</h2>
</td> </td>
<td style="padding-left: 15px; text-align: right; "> <td style="padding-left: 15px; text-align: right; ">
<h2 style="font-weight: normal; ">${amount}</h2> <h2 style="font-weight: normal; ">${average}</h2>
</td> </td>
<td style="padding-left: 15px; text-align: right; "> <td style="padding-left: 15px; text-align: right; ">
<h2 style="font-weight: normal; ">${total}</h2> <h2 style="font-weight: normal; ">${total}</h2>

View file

@ -1,6 +1,6 @@
<tr> <tr>
<th>Category</th> <th>Category</th>
<th>Amount</th> <th>Average</th>
<th>Total</th> <th>Total</th>
<th>Jan</th> <th>Jan</th>
<th>Feb</th> <th>Feb</th>