Compare commits

..

16 Commits

Author SHA1 Message Date
b9072fc8e8 Set version in about page from pom.xml variable. 2026-03-02 10:53:26 +01:00
7763284ae3 Add an help manual. 2026-03-02 10:23:33 +01:00
0873432a96 Add about information. 2026-03-02 09:22:52 +01:00
838069e1b9 Ignore empty lines or comments in vacation point file. 2026-03-02 07:20:58 +01:00
d71abe29b0 Add support for free days off. 2026-03-01 10:19:29 +01:00
576ab90180 Optimize getting of points. 2026-03-01 09:52:21 +01:00
4a91c4bbc9 Simplify execution with Maven instruction. 2026-02-28 21:58:06 +01:00
2db714d353 Implement importing points. 2026-02-28 21:52:23 +01:00
23a8fc5f3e Change exitButton to exitItem. 2026-02-27 14:37:58 +01:00
ea5b262610 Remove public interface to set defaultPoints.
This will be set in the imported files.
2026-02-27 14:30:11 +01:00
b7078f4e5d Add a PointEngine to retrieve points. 2026-02-27 14:26:11 +01:00
0908275eba Set special color for selected and today. 2026-02-27 14:13:43 +01:00
0a0a602a11 Add override to actionPerformed(). 2026-02-27 14:09:20 +01:00
b8e69b3549 Add exit menu button. 2026-02-27 13:49:45 +01:00
909a29d5e3 Allow color change of 'today' when selected. 2026-02-27 13:19:01 +01:00
668fc0f63d Remove trailing whitespace. 2026-02-27 13:10:47 +01:00
9 changed files with 298 additions and 12 deletions

View File

@@ -24,7 +24,7 @@ mvn package
Using Maven:
```bash
mvn exec:java@com.calendar.CalendarApp
mvn exec:java
```
Using Java directly:

View File

@@ -29,6 +29,12 @@
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@@ -10,12 +10,14 @@ import java.util.HashSet;
import java.util.Set;
public class CalendarPanel extends JPanel {
private PointEngine pointEngine;
private YearMonth currentMonth;
private final int cellHeight = 60;
private final int cellWidth = 80;
private final Set<LocalDate> selectedDates = new HashSet<>();
public CalendarPanel() {
public CalendarPanel(PointEngine pointEngine) {
this.pointEngine = pointEngine;
this.currentMonth = YearMonth.now();
setPreferredSize(new Dimension(7 * cellWidth, 8 * cellHeight));
setBackground(Color.WHITE);
@@ -67,6 +69,9 @@ public class CalendarPanel extends JPanel {
}
private void toggleSelection(LocalDate date) {
if (pointEngine.getPointsOfDay(date) == 0.0)
return;
if (selectedDates.contains(date)) {
selectedDates.remove(date);
} else {
@@ -77,7 +82,13 @@ public class CalendarPanel extends JPanel {
}
public double getTotalPoints() {
return selectedDates.size() * 1.0;
double totalPoints = 0.0;
for (LocalDate date : selectedDates) {
totalPoints += pointEngine.getPointsOfDay(date);
}
return totalPoints;
}
private LocalDate getDateAtPoint(int x, int y) {
@@ -148,13 +159,18 @@ public class CalendarPanel extends JPanel {
LocalDate cellDate = currentMonth.atDay(day);
boolean isToday = cellDate.equals(today);
boolean isSelected = selectedDates.contains(cellDate);
double points = pointEngine.getPointsOfDay(cellDate);
if (isSelected) {
if (points == 0.0) {
g.setColor(new Color(255, 100, 100));
g.fillRect(x, y, cellWidth, cellHeight);
} else if (isSelected && isToday) {
g.setColor(new Color(123, 166, 180));
g.fillRect(x, y, cellWidth, cellHeight);
} else if (isSelected) {
g.setColor(new Color(173, 216, 230));
g.fillRect(x, y, cellWidth, cellHeight);
}
if (isToday) {
} else if (isToday) {
g.setColor(new Color(255, 200, 100));
g.fillRect(x, y, cellWidth, cellHeight);
}
@@ -167,7 +183,7 @@ public class CalendarPanel extends JPanel {
int textY = y + (cellHeight - fm.getHeight()) / 2 + fm.getAscent() - 8;
g.drawString(dayStr, textX, textY);
String pointsStr = "(1.0)";
String pointsStr = "(" + points + ")";
Font smallFont = new Font("Arial", Font.PLAIN, 10);
FontMetrics smallFm = g.getFontMetrics(smallFont);
g.setFont(smallFont);

View File

@@ -0,0 +1,110 @@
package net.themusicinnoise.vaccalc;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PointEngine {
private double defaultPoints;
static private class PointRule {
private enum RuleType {
DAY_OF_WEEK,
MONTH,
DATE,
}
static final Pattern DOW_PATTERN = Pattern.compile("dow=(sun|mon|tue|wed|thu|fri|sat) (\\d+\\.\\d+)", Pattern.CASE_INSENSITIVE);
static final Pattern MONTH_PATTERN = Pattern.compile("m=(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) (\\d+\\.\\d+)", Pattern.CASE_INSENSITIVE);
static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2}) (\\d+\\.\\d+)", Pattern.CASE_INSENSITIVE);
private RuleType type;
private DayOfWeek dow;
private Month mon;
private LocalDate date;
private double points;
public PointRule(String def) {
Matcher dowMatcher = DOW_PATTERN.matcher(def);
Matcher monthMatcher = MONTH_PATTERN.matcher(def);
Matcher dateMatcher = DATE_PATTERN.matcher(def);
if (dowMatcher.find()) {
this.type = RuleType.DAY_OF_WEEK;
this.points = Double.parseDouble(dowMatcher.group(2));
this.dow = DayOfWeek.from(DateTimeFormatter.ofPattern("EEE").parse(dowMatcher.group(1)));
} else if (monthMatcher.find()) {
this.type = RuleType.MONTH;
this.points = Double.parseDouble(monthMatcher.group(2));
this.mon = Month.from(DateTimeFormatter.ofPattern("MMM").parse(monthMatcher.group(1)));
} else if (dateMatcher.find()) {
this.type = RuleType.DATE;
this.points = Double.parseDouble(dateMatcher.group(2));
this.date = LocalDate.parse(dateMatcher.group(1), DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} else {
throw new IllegalArgumentException("Invalid line: '" + def + "'");
}
}
public boolean applies(LocalDate date) {
switch (type) {
case DAY_OF_WEEK:
return this.dow == date.getDayOfWeek();
case MONTH:
return this.mon == date.getMonth();
case DATE:
return this.date.isEqual(date);
}
return false;
}
public double getPoints() { return points; }
}
private List<PointRule> rules;
public PointEngine() {
defaultPoints = 1.0;
rules = new ArrayList<>();
}
public void importPointsFile(File pointsFile) {
Pattern defaultPattern = Pattern.compile("default (\\d+\\.\\d+)");
try (BufferedReader br = new BufferedReader(new FileReader(pointsFile))) {
String line;
while ((line = br.readLine()) != null) {
if (line.isEmpty() || line.charAt(0) == '#')
continue;
Matcher defaultMatcher = defaultPattern.matcher(line);
if (defaultMatcher.find()) {
defaultPoints = Double.parseDouble(defaultMatcher.group(1));
} else {
rules.addFirst(new PointRule(line));
}
}
} catch (IOException e) {
throw new RuntimeException("Failed to read points file: " + pointsFile, e);
}
}
public void reset() { rules.clear(); }
public double getPointsOfDay(LocalDate date) {
for (PointRule rule : rules) {
if (rule.applies(date)) {
return rule.getPoints();
}
}
return defaultPoints;
}
}

View File

@@ -2,12 +2,17 @@ package net.themusicinnoise.vaccalc;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.time.YearMonth;
import java.util.Properties;
import java.util.stream.Collectors;
public class VacCalc extends JFrame {
private CalendarPanel calendarPanel;
private JLabel monthLabel;
private JLabel pointsLabel;
private PointEngine pointEngine;
public VacCalc() {
setTitle("VacCalc");
@@ -15,7 +20,77 @@ public class VacCalc extends JFrame {
setResizable(false);
setLocationRelativeTo(null);
calendarPanel = new CalendarPanel();
JMenuBar menuBar = new JMenuBar();
JMenu appMenu = new JMenu("VacCalc");
JMenuItem importItem = new JMenuItem("Import points");
importItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ev) {
JFileChooser fileChooser = new JFileChooser();
int ret = fileChooser.showOpenDialog(VacCalc.this);
if(ret == JFileChooser.APPROVE_OPTION) {
try {
pointEngine.importPointsFile(fileChooser.getSelectedFile());
} catch(RuntimeException ex) {
ex.printStackTrace();
JOptionPane.showMessageDialog(VacCalc.this, ex.getMessage(), "Parsing Error",
JOptionPane.ERROR_MESSAGE);
pointEngine.reset();
}
}
calendarPanel.repaint();
}
});
appMenu.add(importItem);
JMenuItem exitItem = new JMenuItem("Exit");
exitItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
appMenu.add(exitItem);
menuBar.add(appMenu);
JMenu helpMenu = new JMenu("Help");
JMenuItem manualItem = new JMenuItem("Usage Manual");
manualItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ev) {
BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("manual.html")));
String manualText = br.lines().collect(Collectors.joining());
JEditorPane textArea = new JEditorPane("text/html", manualText);
textArea.setEditable(false);
JFrame manualFrame = new JFrame("VacCalc Manual");
manualFrame.getContentPane().add(new JScrollPane(textArea));
manualFrame.setSize(500, 500);
manualFrame.setVisible(true);
}
});
helpMenu.add(manualItem);
JMenuItem aboutItem = new JMenuItem("About");
aboutItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ev) {
BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("about.html")));
Properties properties = new Properties();
try {
properties.load(getClass().getClassLoader().getResourceAsStream("project.properties"));
} catch (IOException e) {
System.err.println("Failed to load project properties.");
e.printStackTrace();
}
String aboutText = br.lines().collect(Collectors.joining());
aboutText = aboutText.replace("VERSION", properties.getProperty("version"));
JOptionPane.showMessageDialog(VacCalc.this, aboutText, "About",
JOptionPane.INFORMATION_MESSAGE);
}
});
helpMenu.add(aboutItem);
menuBar.add(helpMenu);
setJMenuBar(menuBar);
pointEngine = new PointEngine();
calendarPanel = new CalendarPanel(pointEngine);
JPanel headerPanel = createHeaderPanel();
JPanel footerPanel = createFooterPanel();
@@ -94,7 +169,7 @@ public class VacCalc extends JFrame {
private void updatePointsLabel() {
double points = calendarPanel.getTotalPoints();
pointsLabel.setText(String.format("Total Points: %.1f", points));
pointsLabel.setText(String.format("Total Points: %.3f", points));
}
public static void main(String[] args) {

View File

@@ -0,0 +1,7 @@
<html>
<p><b>VacCalc VERSION</b></p>
<hr />
<p>Copyright &copy; 2026 Nicolás A. Ortega Froysa &lt;nicolas@ortegas.org&gt;</p>
<p>License: Zlib</p>
<p>Home page: <a href="https://code.ortegas.org/nortega/vaccalc" >https://code.ortegas.org/nortega/vaccalc</a></p>
</html>

View File

@@ -0,0 +1,59 @@
<html>
<h1>VacCalc Manual</h1>
<hr />
<p>VacCalc assigns points to each day on the basis of the rules found in a
file which is imported via the “VacCalc &gt; Import points” menu item. This
file must be written manually. What follows are descriptions on how to write
this file.</p>
<h2>Rules</h2>
<p>There are a total of four different kinds of rules possible: default,
date, day-of-week, and month. Rules are applied with a priority for the last
defined rule, with the exception of the default rule which is only used if
no other rule applies.</p>
<h3>Default Rule</h3>
<p>Definition: <code>default <i>points</i></code></p>
<p>The default rule establishes the default point value for every day of the
year. E.g. <code>default 1.0</code> sets the default points to
<code>1.0</code>.
<h3>Date Rule</h3>
<p>Definition: <code><i>YYYY</i>-<i>MM</i>-<i>DD</i> <i>points</i></code></p>
<p>A date rule specifies the points which a specific date has. E.g.
<code>2026-12-24 0.5</code> would set Christmas Eve (December
24<sup>th</sup>) of 2026 to <code>0.5</code> points</p>
<h3>Day-Of-Week Rule</h3>
<p>Definition: <code>dow=<i>day</i> <i>points</i></p>
<p>A day-of-week rule specifies the points for all days of the year which
coincide with a particular day of the week. This day of the week is
specified using the three-letter format, such as <code>Fri</code> for
Friday. E.g. <code>dow=Fri 0.8</code> would set all Fridays of the year to
<code>0.8</code> points.</p>
<h3>Month Rule</h3>
<p>Definition: <code>m=<i>month</i> <i>points</i></p>
<p>A month rule establishes the same points for every day of a given month.
E.g. <code>m=Aug 0.8</code> sets all the days of the month of August to
<code>0.8</code> points.
<h3>Zero Point Rules</h3>
<p>Rules that have zero points allocated to them (i.e. <code>0.0</code>) are
considered <em>free days</em>, and as such are disabled. This could be
assigned to days such as Christmas or the Weekends (Saturday and
Sunday).</p>
<h3>Comments</h3>
<p>Definition: <code># <i>comment</i></p>
<p>Comments are supported in single lines starting with the <code>#</code>
character. Everything after that is ignored by the parser.</p>
<h3>Example</h3>
<code>
default 1.0<br />
dow=Fri 0.825<br />
m=Jul 0.825<br />
m=Aug 0.825<br />
2026-12-24 0.5<br />
2026-12-31 0.5<br />
<br />
# Weekends and Holidays<br />
dow=Sun 0.0<br />
dow=Sat 0.0<br />
2026-01-01 0.0<br />
2026-12-25 0.0
</code>
</html>

View File

@@ -0,0 +1 @@
version=${project.version}

12
vac-points.txt Normal file
View File

@@ -0,0 +1,12 @@
default 1.0
dow=Fri 0.825
m=Jul 0.825
m=Aug 0.825
2026-12-24 0.5
2026-12-31 0.5
# Weekends & Holidays
dow=Sun 0.0
dow=Sat 0.0
2026-01-01 0.0
2026-12-25 0.0