The correct Answer by MC Emperor inspired me to take the next step, devising a DayOfWeekTime
class that more flexibly does date-time method by accepting the more general Period
& Duration
class objects rather than just a number of hours.
I leveraged the existing java.time classes as much as possible. I followed the naming conventions of java.time. And I emulated the functionality of java.time.
For strings, I looked to the ISO 8601 standard used by java.time. Unfortunately, the standard does not address the day-of-week with time-of-day concept. The standard does have an idea of week of year, noted with a W
followed by a number from 1-52 or 1-53. For a particular day of a particular week, the standard append a hyphen with a number 1-7 for Monday-Sunday. So I followed that pattern for my toString
and parse
methods. I start with a W
, omit any week number, follow with a hyphen and a day-of-week number 1-7. I append a T
as a separator, following the lead of the standard. Then append the time-of-day in 24-hour clock with padding zero for hour and minute. For example, the date-time of 2020-04-27T13:00-04:00[America/Montreal]
produces W-7T19:46:40.937485
.
This code has been only barely tested. Use at your own risk.
package work.basil.example;
import java.time.*;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAdjusters;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
// Revised 2020-04-27. Fixed bug where the plus & minus methods adjusted from baseline by time-of-day but not day-of-week. Fixed.
public class DayOfWeekTime
{
// ----------| Fields |------------------------------------------
private DayOfWeek dayOfWeek;
private LocalTime localTime;
// ----------| Statics |------------------------------------------
// We do the date-time math by picking a date arbitrarily to use as a `LocalDateTime`.
// For convenience, we might as well pick a year that starts on a Monday.
// https://en.wikipedia.org/wiki/Common_year_starting_on_Monday
// Let us go with 2001-01-01.
static private LocalDateTime BASELINE = LocalDateTime.of( 2001 , 1 , 1 , 0 , 0 , 0 , 0 );
// ----------| Constructor |------------------------------------------
private DayOfWeekTime ( final DayOfWeek dayOfWeek , final LocalTime localTime )
{
Objects.requireNonNull( dayOfWeek , "Received a null argument. Message # e3e04fde-d96a-41d8-a4e7-c2b4f2f5634b." );
Objects.requireNonNull( localTime , "Received a null argument. Message # 97ccf27a-6c05-402a-a4aa-0a48bcff62c2." );
this.dayOfWeek = dayOfWeek;
this.localTime = localTime;
}
// ----------| Factory |------------------------------------------
static public DayOfWeekTime of ( final DayOfWeek dayOfWeek , final LocalTime localTime )
{
Objects.requireNonNull( dayOfWeek , "Received a null argument. Message # ecfe6bf6-de34-4f63-9a3e-d04cd70e721f." );
Objects.requireNonNull( localTime , "Received a null argument. Message # 83020094-409d-40e1-8dc3-12592eea1b81." );
return new DayOfWeekTime( dayOfWeek , localTime );
}
static public DayOfWeekTime now ( ZoneId zoneId )
{
Objects.requireNonNull( zoneId , "Received null argument for time zone. Message # 6044dd82-3616-40a6-8ac2-52581e12e60f." );
ZonedDateTime now = ZonedDateTime.now( zoneId );
DayOfWeek dow = now.getDayOfWeek();
LocalTime lt = now.toLocalTime();
DayOfWeekTime dayOfWeekTime = DayOfWeekTime.of( dow , lt );
return dayOfWeekTime;
}
// ----------| Duration |------------------------------------------
public DayOfWeekTime plus ( final Duration duration )
{
Objects.requireNonNull( duration , "Received a null argument. Message # cf60bd16-3992-4779-a621-a0a3fdb2d750." );
LocalDateTime ldt = DayOfWeekTime.BASELINE.with( TemporalAdjusters.nextOrSame( this.dayOfWeek ) ).with( this.localTime );
LocalDateTime ldtSum = ldt.plus( duration );
DayOfWeekTime dayOfWeekTime = DayOfWeekTime.of( ldtSum.getDayOfWeek() , ldtSum.toLocalTime() );
return dayOfWeekTime;
}
public DayOfWeekTime minus ( final Duration duration )
{
Objects.requireNonNull( duration , "Received a null argument. Message # 4e7bf8c9-6e4f-4e3f-a8b1-5a42dc23cd8a." );
LocalDateTime ldt = DayOfWeekTime.BASELINE.with( TemporalAdjusters.nextOrSame( this.dayOfWeek ) ).with( this.localTime );
LocalDateTime ldtSum = ldt.minus( duration );
DayOfWeekTime dayOfWeekTime = DayOfWeekTime.of( ldtSum.getDayOfWeek() , ldtSum.toLocalTime() );
return dayOfWeekTime;
}
// ----------| Period |------------------------------------------
public DayOfWeekTime plus ( final Period period )
{
Objects.requireNonNull( period , "Received a null argument. Message # 3b1f65b0-5b2c-4e86-aaa3-527992356d32." );
LocalDateTime ldt = DayOfWeekTime.BASELINE.with( TemporalAdjusters.nextOrSame( this.dayOfWeek ) ).with( this.localTime );
LocalDateTime ldtSum = ldt.plus( period );
DayOfWeekTime dayOfWeekTime = DayOfWeekTime.of( ldtSum.getDayOfWeek() , ldtSum.toLocalTime() );
return dayOfWeekTime;
}
public DayOfWeekTime minus ( final Period period )
{
Objects.requireNonNull( period , "Received a null argument. Message # 045938fc-d4b2-4bd2-8803-91db54d92564." );
LocalDateTime ldt = DayOfWeekTime.BASELINE.with( TemporalAdjusters.nextOrSame( this.dayOfWeek ) ).with( this.localTime );
LocalDateTime ldtSum = ldt.minus( period );
DayOfWeekTime dayOfWeekTime = DayOfWeekTime.of( ldtSum.getDayOfWeek() , ldtSum.toLocalTime() );
return dayOfWeekTime;
}
// ----------| Parsing |------------------------------------------
// This text output invented here in this method is styled to follow the designs of ISO 8601,
// but is most certainly *not* defined in the standard.
static public DayOfWeekTime parse ( final String input )
{
Objects.requireNonNull( input , "Received a null argument. Message # 7c519b65-a1ec-486b-a9e9-ff31ee1b8057." );
if ( input.isEmpty() || input.isBlank() )
{
throw new IllegalArgumentException( "Received empty/blank string as argument. Message # 59993300-bdf9-4e69-82e0-823456715c60." );
}
DayOfWeek dayOfWeek = null;
LocalTime localTime = null;
// My regex powers are weak.
// My half-baked scheme returns tokens = [, -1, 13:00] for an input of "W-1T13:00".
String delimiters = "[WT]+";
String[] tokens = input.toUpperCase( Locale.US ).split( delimiters ); // ISO 8601 requires the output of uppercase letters while mandating that we accept lowercase.
System.out.println( "DEBUG tokens = " + Arrays.toString( tokens ) );
if ( tokens.length != 3 )
{
throw new IllegalArgumentException( "Received invalid string as argument. Message # e521a4e3-1ee5-46e9-b351-a5edb7206b82." );
}
int dowNumber = Math.abs( Integer.parseInt( tokens[ 1 ] ) );
dayOfWeek = DayOfWeek.of( dowNumber );
String localTimeInput = Objects.requireNonNull( tokens[ 2 ] , "The time-of-day component of the input is null. Message # 1faed491-abaa-42bd-b767-d876f8ba07d9." );
if ( localTimeInput.isEmpty() || localTimeInput.isBlank() )
{
throw new IllegalArgumentException( "The time-of-day component of the input is empty/blank. Message # 98208025-d7c2-4b59-bc5f-fed7bec09741." );
}
try
{
localTime = LocalTime.parse( localTimeInput );
}
catch ( DateTimeParseException e )
{
throw new IllegalArgumentException( "The time-of-day component of the input is invalid. Message # 8de7e8d8-f4a3-478d-96d8-911454aced14." );
}
DayOfWeekTime dayOfWeekTime = DayOfWeekTime.of( dayOfWeek , localTime );
return dayOfWeekTime;
}
// ----------| Moment |------------------------------------------
public ZonedDateTime atDateInZone ( LocalDate localDate , ZoneId zoneId )
{
Objects.requireNonNull( zoneId , "Received null argument. Message # b8f70601-2b1d-4321-a57f-96384759a960." );
LocalDate ld = localDate.with( TemporalAdjusters.nextOrSame( this.dayOfWeek ) ); // Move to the next date with a matching day-of-week if not a match.
LocalDateTime ldt = LocalDateTime.of( ld , this.localTime );
ZonedDateTime zdt = ldt.atZone( zoneId );
return zdt;
}
// ----------| Accessors |------------------------------------------
public DayOfWeek getDayOfWeek ( ) { return this.dayOfWeek; }
public LocalTime getLocalTime ( ) { return this.localTime; }
// ----------| Object |------------------------------------------
@Override
public boolean equals ( Object o )
{
if ( this == o ) return true;
if ( o == null || getClass() != o.getClass() ) return false;
DayOfWeekTime that = ( DayOfWeekTime ) o;
return dayOfWeek == that.dayOfWeek &&
localTime.equals( that.localTime );
}
@Override
public int hashCode ( )
{
return Objects.hash( dayOfWeek , localTime );
}
// This text output invented here in this method is styled to follow the designs of ISO 8601,
// but is most certainly *not* defined in the standard.
@Override
public String toString ( )
{
String output = "W" + "-" + this.dayOfWeek.getValue() + "T" + this.localTime.toString();
return output;
}
}
Example usage for Sunday.
DayOfWeekTime dayOfWeekTime = DayOfWeekTime.parse( "W-7T13:00" );
DayOfWeekTime nineDaysPrior = dayOfWeekTime.minus( Period.ofDays( 9 ) );
DayOfWeekTime oneDayPrior = dayOfWeekTime.minus( Period.ofDays( 1 ) );
DayOfWeekTime oneDayLater = dayOfWeekTime.plus( Period.ofDays( 1 ) );
DayOfWeekTime twoDaysLater = dayOfWeekTime.plus( Period.ofDays( 2 ) );
DayOfWeekTime weekLater = dayOfWeekTime.plus( Period.ofWeeks( 1 ) );
DayOfWeekTime nineDaysLater = dayOfWeekTime.plus( Period.ofDays( 9 ) );
DayOfWeekTime twoHoursLater = dayOfWeekTime.plus( Duration.ofHours( 2 ) );
DayOfWeekTime nineHoursLater = dayOfWeekTime.plus( Duration.ofHours( 9 ) );
DayOfWeekTime twentyFourHoursLater = dayOfWeekTime.plus( Duration.ofHours( 24 ) );
ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = dayOfWeekTime.atDateInZone( LocalDate.now( z ) , z );
DayOfWeekTime now = DayOfWeekTime.now( z );
Dump to console.
System.out.println( "After parsing: dayOfWeekTime = " + dayOfWeekTime );
System.out.println( "nineDaysPrior = " + nineDaysPrior );
System.out.println( "oneDayPrior = " + oneDayPrior );
System.out.println( "oneDayLater = " + oneDayLater );
System.out.println( "twoDaysLater = " + twoDaysLater );
System.out.println( "weekLater = " + weekLater );
System.out.println( "nineDaysLater = " + nineDaysLater );
System.out.println( "twoHoursLater = " + twoHoursLater );
System.out.println( "nineHoursLater = " + nineHoursLater );
System.out.println( "twentyFourHoursLater = " + twentyFourHoursLater );
System.out.println( "zdt = " + zdt );
System.out.println( "now = " + now );
DEBUG tokens = [, -7, 13:00]
After parsing: dayOfWeekTime = W-7T13:00
nineDaysPrior = W-5T13:00
oneDayPrior = W-6T13:00
oneDayLater = W-1T13:00
twoDaysLater = W-2T13:00
weekLater = W-7T13:00
nineDaysLater = W-2T13:00
twoHoursLater = W-7T15:00
nineHoursLater = W-7T22:00
twentyFourHoursLater = W-1T13:00
zdt = 2020-05-03T13:00-04:00[America/Montreal]
now = W-1T16:10:33.248253
And for Monday.
DayOfWeekTime dayOfWeekTime = DayOfWeekTime.parse( "W-1T13:00" );
…
DEBUG tokens = [, -1, 13:00]
After parsing: dayOfWeekTime = W-1T13:00
nineDaysPrior = W-6T13:00
oneDayPrior = W-7T13:00
oneDayLater = W-2T13:00
twoDaysLater = W-3T13:00
weekLater = W-1T13:00
nineDaysLater = W-3T13:00
twoHoursLater = W-1T15:00
nineHoursLater = W-1T22:00
twentyFourHoursLater = W-2T13:00
zdt = 2020-04-27T13:00-04:00[America/Montreal]
now = W-1T16:16:11.543665
Seems like this functionality really could be useful to people, for handling workers’ shifts, or store opening/closing times. If this makes sense to others, perhaps someone would care to help on this pull-request feature on the ThreeTen-Extra project.
DayOfWeek newDayOfWeek = this.dayOfWeek.plus(plusDays);
.DayOfWeek.plus()
has cyclic overflow, so for exampleSATURDAY.plus(2)
yieldsMONDAY
. – Coopt