Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/org/uwpr/instrumentlog/UsageBlockBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ public void setSetupBlock(boolean setupBlock) {
this.setupBlock = setupBlock;
}

/**
* Returns the elapsed hours from start to end. Across a daylight saving change this is one hour less or
* more than the clock times. Billing, the instrument time quota and the scheduled time totals use this count.
*/
public int getHours()
{
return TimeUtils.getHours(getStartDate(), getEndDate());
Expand Down
65 changes: 65 additions & 0 deletions src/org/uwpr/instrumentlog/UsageBlockBaseTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.uwpr.instrumentlog;

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import junit.framework.TestCase;

/**
* getHours() must count elapsed hours. A block across a daylight saving change counts one hour less or more
* than its clock times.
*/
public class UsageBlockBaseTest extends TestCase {

private TimeZone defaultZone;

protected void setUp() throws Exception {
super.setUp();
defaultZone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
}

protected void tearDown() throws Exception {
TimeZone.setDefault(defaultZone);
super.tearDown();
}

public final void testHoursWithNoClockChange() {
UsageBlockBase block = block(2026, 9, 16, 22, 2026, 9, 17, 6);
assertEquals("10 PM to 6 AM with no clock change should be 8 hours", 8, block.getHours());
}

public final void testHoursAcrossSpringForward() {
// Clocks go from 2 AM to 3 AM on 2026-03-08.
UsageBlockBase block = block(2026, 3, 7, 22, 2026, 3, 8, 6);
assertEquals("10 PM to 6 AM across spring forward should be 7 hours", 7, block.getHours());
}

public final void testHoursAcrossFallBack() {
// Clocks go from 2 AM back to 1 AM on 2026-11-01.
UsageBlockBase block = block(2026, 10, 31, 22, 2026, 11, 1, 6);
assertEquals("10 PM to 6 AM across fall back should be 9 hours", 9, block.getHours());
}

public final void testHoursOfAMultiDayBlockAcrossFallBack() {
// Block 24951 on the dev copy, billed 29 hours.
UsageBlockBase block = block(2025, 11, 1, 0, 2025, 11, 2, 4);
assertEquals("Midnight Nov 1 to 4 AM Nov 2 2025, across fall back, should be 29 hours", 29, block.getHours());
}

private static UsageBlockBase block(int startYear, int startMonth, int startDay, int startHour,
int endYear, int endMonth, int endDay, int endHour) {
UsageBlockBase block = new UsageBlockBase();
block.setStartDate(date(startYear, startMonth, startDay, startHour));
block.setEndDate(date(endYear, endMonth, endDay, endHour));
return block;
}

private static Date date(int year, int month, int day, int hour) {
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(year, month - 1, day, hour, 0, 0);
return calendar.getTime();
}
}
14 changes: 12 additions & 2 deletions src/org/uwpr/www/instrumentlog/JSONInstrumentUsageGetter.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ private JSONObject getForContiguousBlocks(List<UsageBlock> blocks,
event.put("projectId", Integer.valueOf(blocks.get(0).getProjectID()));
event.put("instrumentId", Integer.valueOf(blocks.get(0).getInstrumentID()));
event.put("title", String.valueOf(blocks.get(0).getProjectID()));
event.put("start", sd.toString());
event.put("end", ed.toString());
event.put("start", formatEventTime(sd));
event.put("end", formatEventTime(ed));
event.put("allDay", Boolean.FALSE);
event.put("editable", Boolean.FALSE);

Expand Down Expand Up @@ -363,6 +363,16 @@ private JSONObject getForContiguousBlocks(List<UsageBlock> blocks,
return event;
}

/**
* Formats a block time for fullCalendar as the server's local date and time with no zone, for example
* 2026-09-17T10:00:00. fullCalendar displays a time with no zone at that hour in every browser time zone.
* Date.toString() carries the JVM's zone, and a browser in another time zone converts it to its own hour.
*/
static String formatEventTime(Date date)
{
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date);
}

private Object addToCalenderLink(UsageBlock block) throws UnsupportedEncodingException {
/*
https://www.google.com/calendar/render?action=TEMPLATE&text=UWPR&dates=20140127T224000Z/20140320T221500Z&details=Some+Details&sf=true&output=xml
Expand Down
64 changes: 64 additions & 0 deletions src/org/uwpr/www/instrumentlog/JSONInstrumentUsageGetterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.uwpr.www.instrumentlog;

import java.time.Instant;
import java.util.Date;
import java.util.TimeZone;

import junit.framework.TestCase;

/**
* formatEventTime() must return a block time as the wall-clock time in the server's zone, the zone the block's
* label is formatted in, with no zone in the string. Each test starts from a fixed moment in UTC, so the
* expected hour depends on the server's zone.
*/
public class JSONInstrumentUsageGetterTest extends TestCase {

private TimeZone defaultZone;

protected void setUp() throws Exception {
super.setUp();
defaultZone = TimeZone.getDefault();
}

protected void tearDown() throws Exception {
TimeZone.setDefault(defaultZone);
super.tearDown();
}

public final void testEventTimeOnALosAngelesServer() {
TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
assertEquals("17:00 UTC on 2026-09-17 should be sent as 10:00 from a Los Angeles server (UTC - 7)",
"2026-09-17T10:00:00", JSONInstrumentUsageGetter.formatEventTime(utc("2026-09-17T17:00:00Z")));
}

public final void testEventTimeOnALosAngelesServerAfterFallBack() {
TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
assertEquals("14:00 UTC on 2026-11-01, after the clocks go back, "
+ "should be sent as 06:00 from a Los Angeles server (UTC - 8)",
"2026-11-01T06:00:00", JSONInstrumentUsageGetter.formatEventTime(utc("2026-11-01T14:00:00Z")));
}

public final void testEventTimeOnANewYorkServer() {
TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"));
assertEquals("17:00 UTC on 2026-09-17 should be sent as 13:00 from a New York server (UTC - 4)",
"2026-09-17T13:00:00", JSONInstrumentUsageGetter.formatEventTime(utc("2026-09-17T17:00:00Z")));
}

public final void testEventTimeOnAKolkataServer() {
// Kolkata has no daylight saving time and a half-hour offset from UTC.
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Kolkata"));
assertEquals("17:00 UTC on 2026-09-17 should be sent as 22:30 from a Kolkata server (UTC + 5:30)",
"2026-09-17T22:30:00", JSONInstrumentUsageGetter.formatEventTime(utc("2026-09-17T17:00:00Z")));
}

public final void testEventTimeOnAServerWithAFixedOffset() {
// A fixed offset has no daylight saving time, so in September it is an hour behind Pacific time.
TimeZone.setDefault(TimeZone.getTimeZone("GMT-08:00"));
assertEquals("17:00 UTC on 2026-09-17 should be sent as 09:00 from a GMT-08:00 server (UTC - 8)",
"2026-09-17T09:00:00", JSONInstrumentUsageGetter.formatEventTime(utc("2026-09-17T17:00:00Z")));
}

private static Date utc(String isoInstant) {
return Date.from(Instant.parse(isoInstant));
}
}
19 changes: 3 additions & 16 deletions src/org/uwpr/www/util/TimeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,6 @@ public static Date makeBeginningOfDay(Date date)
return startCal.getTime();
}

// public static Date makeEndOfDay(Date date)
// {
// if(date == null)
// {
// return null;
// }
// Calendar endCal = Calendar.getInstance();
// endCal.setTime(date);
// endCal.set(Calendar.MILLISECOND, 0);
// endCal.set(Calendar.SECOND, 0);
// endCal.set(Calendar.MINUTE, 0);
// endCal.set(Calendar.HOUR_OF_DAY, 0); // 12:00 am
// return new Date(endCal.getTime().getTime() + MILLIS_IN_DAY - 1);
// }

public static Date makeEndOfDay_12AM(Date date)
{
if(date == null)
Expand All @@ -63,7 +48,9 @@ public static Date makeEndOfDay_12AM(Date date)
endCal.set(Calendar.SECOND, 0);
endCal.set(Calendar.MINUTE, 0);
endCal.set(Calendar.HOUR_OF_DAY, 0); // 12:00 am
return new Date(endCal.getTime().getTime() + MILLIS_IN_DAY);
// Add a calendar day, not 24 hours. The day the clocks change is 23 or 25 hours long.
endCal.add(Calendar.DAY_OF_MONTH, 1);
return endCal.getTime();
}

public static String format(Date date)
Expand Down
54 changes: 54 additions & 0 deletions src/org/uwpr/www/util/TimeUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.uwpr.www.util;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import junit.framework.TestCase;

/**
* makeEndOfDay_12AM() is the end boundary of the billing export and the scheduled-time views, so it must be
* midnight at the start of the next day, including on a day when the clocks change.
*/
public class TimeUtilsTest extends TestCase {

private TimeZone defaultZone;

protected void setUp() throws Exception {
super.setUp();
defaultZone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
}

protected void tearDown() throws Exception {
TimeZone.setDefault(defaultZone);
super.tearDown();
}

public final void testEndOfAnOrdinaryDay() {
assertEquals("The end of 2026-09-17 should be midnight on 2026-09-18",
"2026-09-18 00:00", format(TimeUtils.makeEndOfDay_12AM(date(2026, 9, 17, 10))));
}

public final void testEndOfSpringForwardDay() {
assertEquals("The end of 2026-03-08, when clocks go forward, should be midnight on 2026-03-09",
"2026-03-09 00:00", format(TimeUtils.makeEndOfDay_12AM(date(2026, 3, 8, 10))));
}

public final void testEndOfFallBackDay() {
assertEquals("The end of 2026-11-01, when clocks go back, should be midnight on 2026-11-02",
"2026-11-02 00:00", format(TimeUtils.makeEndOfDay_12AM(date(2026, 11, 1, 10))));
}

private static String format(Date date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm").format(date);
}

private static Date date(int year, int month, int day, int hour) {
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(year, month - 1, day, hour, 0, 0);
return calendar.getTime();
}
}