Added more query utils, aggregate stats, and randomized testing data.
This commit is contained in:
parent
581eebadbd
commit
bb5370e92a
|
@ -1,7 +1,7 @@
|
|||
package com.github.andrewlalis.running_every_day;
|
||||
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import com.github.andrewlalis.running_every_day.data.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.db.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.view.RecorderAppWindow;
|
||||
import com.github.andrewlalis.running_every_day.view.WindowDataSourceCloser;
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.github.andrewlalis.running_every_day.data;
|
||||
|
||||
import com.github.andrewlalis.running_every_day.data.db.ResultSetMapper;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.sql.ResultSet;
|
||||
|
@ -46,6 +48,16 @@ public record RunRecord(
|
|||
return weightKg.multiply(BigDecimal.valueOf(1000));
|
||||
}
|
||||
|
||||
public Duration averagePacePerKm() {
|
||||
long msPerKm = (long) Math.floor(duration.toMillis() / distanceKm.doubleValue());
|
||||
return Duration.ofMillis(msPerKm);
|
||||
}
|
||||
|
||||
public String averagePacePerKmFormatted() {
|
||||
var dur = averagePacePerKm();
|
||||
return String.format("%02d:%02d.%03d", dur.toMinutesPart(), dur.toSecondsPart(), dur.toMillisPart());
|
||||
}
|
||||
|
||||
public static class Mapper implements ResultSetMapper<RunRecord> {
|
||||
@Override
|
||||
public RunRecord map(ResultSet rs) throws SQLException {
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
package com.github.andrewlalis.running_every_day.data;
|
||||
|
||||
import com.github.andrewlalis.running_every_day.data.db.Page;
|
||||
import com.github.andrewlalis.running_every_day.data.db.Pagination;
|
||||
import com.github.andrewlalis.running_every_day.data.db.Queries;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data access object for interacting with run records in the database.
|
||||
* @param conn The database connection.
|
||||
*/
|
||||
public record RunRecordRepository(Connection conn) {
|
||||
public Page<RunRecord> findAll(Pagination pagination) throws SQLException {
|
||||
String query = "SELECT * FROM run ORDER BY date ASC";
|
||||
|
@ -62,22 +73,20 @@ public record RunRecordRepository(Connection conn) {
|
|||
}
|
||||
}
|
||||
|
||||
public long countAll() throws SQLException {
|
||||
String query = "SELECT COUNT(id) FROM run";
|
||||
try (
|
||||
var stmt = conn.prepareStatement(query);
|
||||
var rs = stmt.executeQuery()
|
||||
) {
|
||||
if (rs.next()) {
|
||||
return rs.getLong(1);
|
||||
} else {
|
||||
throw new SQLException("Missing count.");
|
||||
}
|
||||
}
|
||||
public long pageCount(int pageSize) throws SQLException {
|
||||
return Queries.pageCount(conn, "SELECT COUNT(id) FROM run", pageSize);
|
||||
}
|
||||
|
||||
public long pageCount(int size) throws SQLException {
|
||||
long recordCount = countAll();
|
||||
return (recordCount / size) + (recordCount % size != 0 ? 1 : 0);
|
||||
// Aggregate stats:
|
||||
|
||||
public BigDecimal getTotalDistanceKm() throws SQLException {
|
||||
long totalDistanceMeters = Queries.getLong(conn, "SELECT SUM(distance) FROM run");
|
||||
return BigDecimal.valueOf(totalDistanceMeters, 3)
|
||||
.divide(BigDecimal.valueOf(1000, 3), RoundingMode.UNNECESSARY);
|
||||
}
|
||||
|
||||
public Duration getTotalDuration() throws SQLException {
|
||||
long totalDurationSeconds = Queries.getLong(conn, "SELECT SUM(duration) FROM run");
|
||||
return Duration.ofSeconds(totalDurationSeconds);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
package com.github.andrewlalis.running_every_day.data;
|
||||
package com.github.andrewlalis.running_every_day.data.db;
|
||||
|
||||
import com.github.andrewlalis.running_every_day.data.RunRecordRepository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
@ -6,8 +8,6 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A single object that serves as the application's data source.
|
||||
|
@ -24,14 +24,8 @@ public class DataSource {
|
|||
return new RunRecordRepository(this.conn);
|
||||
}
|
||||
|
||||
public <T> List<T> query(String query, ResultSetMapper<T> mapper) throws SQLException {
|
||||
try (var stmt = conn.prepareStatement(query); var rs = stmt.executeQuery()) {
|
||||
List<T> items = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
items.add(mapper.map(rs));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
public Connection conn() {
|
||||
return conn;
|
||||
}
|
||||
|
||||
public void close() throws SQLException {
|
||||
|
@ -64,7 +58,7 @@ public class DataSource {
|
|||
}
|
||||
}
|
||||
|
||||
private static String readResource(String name) {
|
||||
public static String readResource(String name) {
|
||||
try (var in = DataSource.class.getClassLoader().getResourceAsStream(name)) {
|
||||
if (in == null) {
|
||||
throw new RuntimeException("Missing resource: " + name);
|
|
@ -1,4 +1,4 @@
|
|||
package com.github.andrewlalis.running_every_day.data;
|
||||
package com.github.andrewlalis.running_every_day.data.db;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
|
@ -1,4 +1,4 @@
|
|||
package com.github.andrewlalis.running_every_day.data;
|
||||
package com.github.andrewlalis.running_every_day.data.db;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
|
@ -0,0 +1,74 @@
|
|||
package com.github.andrewlalis.running_every_day.data.db;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class Queries {
|
||||
private Queries() {}
|
||||
|
||||
public static <T> Optional<T> findOne(
|
||||
Connection c,
|
||||
String query,
|
||||
StatementModifier modifier,
|
||||
ResultSetMapper<T> mapper
|
||||
) throws SQLException {
|
||||
try (var stmt = c.prepareStatement(query)) {
|
||||
modifier.apply(stmt);
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return Optional.of(mapper.map(rs));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> Optional<T> findOne(Connection c, String query, ResultSetMapper<T> mapper) throws SQLException {
|
||||
try (var stmt = c.prepareStatement(query)) {
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return Optional.of(mapper.map(rs));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T getOne(Connection c, String query, ResultSetMapper<T> mapper) throws SQLException {
|
||||
return findOne(c, query, mapper).orElseThrow(() -> new SQLException("Missing required SQL result."));
|
||||
}
|
||||
|
||||
public static long getLong(Connection c, String query) throws SQLException {
|
||||
return getOne(c, query, rs -> rs.getLong(1));
|
||||
}
|
||||
|
||||
public static String getString(Connection c, String query) throws SQLException {
|
||||
return getOne(c, query, rs -> rs.getString(1));
|
||||
}
|
||||
|
||||
public static long count(Connection c, String query) throws SQLException {
|
||||
try (var stmt = c.prepareStatement(query); var rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return rs.getLong(1);
|
||||
} else {
|
||||
throw new SQLException("Result set for count query is empty.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static long pageCount(Connection c, String query, int pageSize) throws SQLException {
|
||||
long recordCount = count(c, query);
|
||||
long pageCount = recordCount / pageSize;
|
||||
if (recordCount % pageSize != 0) {
|
||||
pageCount++;
|
||||
}
|
||||
return pageCount;
|
||||
}
|
||||
|
||||
public static int update(Connection c, String query) throws SQLException {
|
||||
try (var stmt = c.prepareStatement(query)) {
|
||||
return stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.github.andrewlalis.running_every_day.data;
|
||||
package com.github.andrewlalis.running_every_day.data.db;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
|
@ -0,0 +1,9 @@
|
|||
package com.github.andrewlalis.running_every_day.data.db;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface StatementModifier {
|
||||
void apply(PreparedStatement stmt) throws SQLException;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package com.github.andrewlalis.running_every_day.view;
|
||||
|
||||
import com.github.andrewlalis.running_every_day.data.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.db.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.RunRecord;
|
||||
|
||||
import javax.swing.*;
|
||||
|
@ -15,7 +15,6 @@ import java.time.format.DateTimeFormatter;
|
|||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddRunRecordDialog extends JDialog {
|
||||
private final DataSource dataSource;
|
||||
|
|
|
@ -1,4 +1,55 @@
|
|||
package com.github.andrewlalis.running_every_day.view;
|
||||
|
||||
public class AggregateStatisticsPanel {
|
||||
import com.github.andrewlalis.running_every_day.data.db.DataSource;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class AggregateStatisticsPanel extends JPanel {
|
||||
private final DataSource dataSource;
|
||||
|
||||
private final JTextField totalDistanceField;
|
||||
private final JTextField totalDurationField;
|
||||
|
||||
public AggregateStatisticsPanel(DataSource dataSource) {
|
||||
super(new BorderLayout());
|
||||
this.dataSource = dataSource;
|
||||
|
||||
totalDistanceField = new JTextField();
|
||||
totalDistanceField.setEditable(false);
|
||||
totalDurationField = new JTextField();
|
||||
totalDurationField.setEditable(false);
|
||||
|
||||
JPanel statsPanel = new JPanel(new GridLayout(2, 2));
|
||||
statsPanel.add(new JLabel("Total Distance (Km)"));
|
||||
statsPanel.add(totalDistanceField);
|
||||
statsPanel.add(new JLabel("Total Duration"));
|
||||
statsPanel.add(totalDurationField);
|
||||
this.add(statsPanel, BorderLayout.CENTER);
|
||||
|
||||
JPanel controlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JButton refreshButton = new JButton("Refresh");
|
||||
refreshButton.addActionListener(e -> refreshStats());
|
||||
controlsPanel.add(refreshButton);
|
||||
this.add(controlsPanel, BorderLayout.NORTH);
|
||||
|
||||
SwingUtilities.invokeLater(this::refreshStats);
|
||||
}
|
||||
|
||||
private void refreshStats() {
|
||||
try {
|
||||
totalDistanceField.setText(dataSource.runRecords().getTotalDistanceKm().toPlainString() + " Km");
|
||||
var totalDuration = dataSource.runRecords().getTotalDuration();
|
||||
String durationStr = String.format(
|
||||
"%d days, %d hours, %d minutes",
|
||||
totalDuration.toDaysPart(),
|
||||
totalDuration.toHoursPart(),
|
||||
totalDuration.toMinutesPart()
|
||||
);
|
||||
totalDurationField.setText(durationStr);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.github.andrewlalis.running_every_day.view;
|
||||
|
||||
import com.github.andrewlalis.running_every_day.data.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.db.DataSource;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
@ -18,7 +18,7 @@ public class RecorderAppWindow extends JFrame {
|
|||
private Container buildGui(DataSource dataSource) {
|
||||
JTabbedPane tabbedPane = new JTabbedPane();
|
||||
tabbedPane.addTab("Run Records", new RunRecordsPanel(dataSource));
|
||||
tabbedPane.addTab("Aggregate Statistics", new JPanel());
|
||||
tabbedPane.addTab("Aggregate Statistics", new AggregateStatisticsPanel(dataSource));
|
||||
tabbedPane.addTab("Charts", new ChartsPanel());
|
||||
return tabbedPane;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package com.github.andrewlalis.running_every_day.view;
|
||||
|
||||
import com.github.andrewlalis.running_every_day.data.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.Pagination;
|
||||
import com.github.andrewlalis.running_every_day.data.RunRecord;
|
||||
import com.github.andrewlalis.running_every_day.data.db.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.db.Pagination;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.sql.SQLException;
|
||||
|
@ -88,7 +88,7 @@ public class RunRecordTableModel extends AbstractTableModel {
|
|||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return 7;
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -101,8 +101,9 @@ public class RunRecordTableModel extends AbstractTableModel {
|
|||
case 2 -> r.startTime().toString();
|
||||
case 3 -> r.distanceKm().toPlainString();
|
||||
case 4 -> r.durationFormatted();
|
||||
case 5 -> r.weightKg().toPlainString();
|
||||
case 6 -> r.comment();
|
||||
case 5 -> r.averagePacePerKmFormatted();
|
||||
case 6 -> r.weightKg().toPlainString();
|
||||
case 7 -> r.comment();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
@ -115,8 +116,9 @@ public class RunRecordTableModel extends AbstractTableModel {
|
|||
case 2 -> "Start Time";
|
||||
case 3 -> "Distance (Km)";
|
||||
case 4 -> "Duration";
|
||||
case 5 -> "Weight (Kg)";
|
||||
case 6 -> "Comment";
|
||||
case 5 -> "Pace (Min/Km)";
|
||||
case 6 -> "Weight (Kg)";
|
||||
case 7 -> "Comment";
|
||||
default -> "Unknown Value";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
package com.github.andrewlalis.running_every_day.view;
|
||||
|
||||
import com.github.andrewlalis.running_every_day.data.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.RunRecord;
|
||||
import com.github.andrewlalis.running_every_day.data.db.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.db.Queries;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* A panel for displaying a table view of run records, and various controls for
|
||||
* navigating the data.
|
||||
*/
|
||||
public class RunRecordsPanel extends JPanel {
|
||||
private final DataSource dataSource;
|
||||
|
||||
private final RunRecordTableModel tableModel;
|
||||
private final JTextField currentPageField;
|
||||
private final JButton firstPageButton;
|
||||
|
@ -19,19 +30,22 @@ public class RunRecordsPanel extends JPanel {
|
|||
|
||||
public RunRecordsPanel(DataSource dataSource) {
|
||||
super(new BorderLayout());
|
||||
this.dataSource = dataSource;
|
||||
this.tableModel = new RunRecordTableModel(dataSource);
|
||||
|
||||
var table = new JTable(tableModel);
|
||||
table.getTableHeader().setReorderingAllowed(false);
|
||||
table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
|
||||
table.getColumnModel().getColumn(0).setMaxWidth(40);
|
||||
table.getColumnModel().getColumn(0).setMaxWidth(50);
|
||||
table.getColumnModel().getColumn(1).setMaxWidth(80);
|
||||
table.getColumnModel().getColumn(2).setMaxWidth(80);
|
||||
table.getColumnModel().getColumn(2).setMaxWidth(70);
|
||||
table.getColumnModel().getColumn(3).setMaxWidth(100);
|
||||
table.getColumnModel().getColumn(3).setPreferredWidth(100);
|
||||
table.getColumnModel().getColumn(4).setMaxWidth(80);
|
||||
table.getColumnModel().getColumn(5).setMaxWidth(80);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
table.getColumnModel().getColumn(4).setMaxWidth(60);
|
||||
table.getColumnModel().getColumn(5).setMaxWidth(100);
|
||||
table.getColumnModel().getColumn(5).setPreferredWidth(100);
|
||||
table.getColumnModel().getColumn(6).setMaxWidth(80);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
table.getColumnModel().getColumn(i).setResizable(false);
|
||||
}
|
||||
var scrollPane = new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
|
||||
|
@ -98,6 +112,9 @@ public class RunRecordsPanel extends JPanel {
|
|||
}
|
||||
});
|
||||
actionsPanel.add(addActionButton);
|
||||
JButton generateRandomDataButton = new JButton("Generate Random Data");
|
||||
generateRandomDataButton.addActionListener(e -> generateRandomData());
|
||||
actionsPanel.add(generateRandomDataButton);
|
||||
this.add(actionsPanel, BorderLayout.NORTH);
|
||||
}
|
||||
|
||||
|
@ -108,4 +125,39 @@ public class RunRecordsPanel extends JPanel {
|
|||
lastPageButton.setEnabled(tableModel.canGoToLastPage());
|
||||
currentPageField.setText(String.valueOf(tableModel.getPagination().page() + 1));
|
||||
}
|
||||
|
||||
private void generateRandomData() {
|
||||
try {
|
||||
var c = dataSource.conn();
|
||||
Queries.update(c, "DELETE FROM run");
|
||||
final int daysToDo = 1000;
|
||||
LocalDate date = LocalDate.now().minusDays(daysToDo);
|
||||
Random rand = new Random();
|
||||
c.setAutoCommit(false);
|
||||
for (int i = 0; i < daysToDo; i++) {
|
||||
LocalTime startTime = LocalTime.of(rand.nextInt(5, 22), rand.nextInt(60));
|
||||
BigDecimal distance = BigDecimal.valueOf(rand.nextInt(1000, 15000), 3)
|
||||
.divide(BigDecimal.valueOf(1000, 3), RoundingMode.UNNECESSARY);
|
||||
double avgPage = rand.nextDouble(300.0, 700.0);
|
||||
BigDecimal weight = BigDecimal.valueOf(rand.nextInt(75000, 110000), 3)
|
||||
.divide(BigDecimal.valueOf(1000, 3), RoundingMode.UNNECESSARY);
|
||||
dataSource.runRecords().save(new RunRecord(
|
||||
0,
|
||||
date,
|
||||
startTime,
|
||||
distance,
|
||||
Duration.ofSeconds((long) Math.floor(avgPage * distance.doubleValue())),
|
||||
weight,
|
||||
"Bleh"
|
||||
));
|
||||
date = date.plusDays(1);
|
||||
}
|
||||
c.commit();
|
||||
c.setAutoCommit(true);
|
||||
tableModel.firstPage();
|
||||
updateButtonStates();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.github.andrewlalis.running_every_day.view;
|
||||
|
||||
import com.github.andrewlalis.running_every_day.data.DataSource;
|
||||
import com.github.andrewlalis.running_every_day.data.db.DataSource;
|
||||
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
|
Loading…
Reference in New Issue