tl;dr
Using modern Java language features and classes, define your own enums to represent the seasons and groups.
Schedule.daysForGroupOnDate(
Group.D ,
LocalDate.now()
)
That method yields a Set
of DayOfWeek
enum objects (not mere text!), such as DayOfWeek.TUESDAY
& DayOfWeek.THURSDAY
.
Modern Java
Can anyone suggest a really cool way to do this?
Yes.
Modern Java has built-in classes, collections, and enums to help you with this problem.
The java.time framework built into Java offers the Month
enum and DayOfWeek
enum.
The EnumSet
and EnumMap
provide implementations of Set
and Map
that are optimized for use with enums for fast execution in very little memory.
You can define your own enums to represent your season and your groups (A, B, and so on). The enum facility in Java is far more useful and powerful than seen in other languages. If not familiar, see the Oracle Tutorial.
The simple syntax of defining your own enums actually provides much of the functionality needed to solve this Question, eliminating some complicated coding. New literals syntax for sets and maps in the collections factory methods in Java 9 (JEP 269) makes the code even simpler now.
Here is a complete working app. Note how little code there is for algorithms. Defining your own custom enums does most of the heavy-lifting.
One caveat with this app code: It assumes nothing changes in your business definitions, or at least if there is a change you only care about the current rules, “latest is greatest”. If your rules change over time and you need to represent all the past, present, and future versions, I would build a very different app, probably with a database to store the rules. But this app here solves the Question as asked.
Season
Represent your season as an enum Season
. Each season object holds a List
of Month
enum objects that define the length of that particular season. The new List.of
syntax added to Java 9 defines an immutable list in a literals syntax via static factory methods.
package com.basilbourque.watering;
import java.time.LocalDate;
import java.time.Month;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
public enum Season
{
SPRING( List.of( Month.MARCH , Month.APRIL ) ),
SUMMER( List.of( Month.MAY , Month.JUNE, Month.JULY , Month.AUGUST ) ),
FALL( List.of( Month.SEPTEMBER , Month.OCTOBER ) ),
WINTER( List.of( Month.NOVEMBER , Month.DECEMBER , Month.JANUARY , Month.FEBRUARY ) );
private List< Month > months;
// Constructor
Season ( List < Month > monthsArg )
{
this.months = monthsArg;
}
public List < Month > getMonths ( )
{
return this.months;
}
// For any given month, determine the season.
static public Season ofLocalMonth ( Month monthArg )
{
Season s = null;
for ( Season season : EnumSet.allOf( Season.class ) )
{
if ( season.getMonths().contains( monthArg ) )
{
s = season;
break; // Bail out of this FOR loop.
}
}
return s;
}
// For any given date, determine the season.
static public Season ofLocalDate ( LocalDate localDateArg )
{
Month month = localDateArg.getMonth();
Season s = Season.ofLocalMonth( month );
return s;
}
// Run `main` for demo/testing.
public static void main ( String[] args )
{
// Dump all these enum objects to console.
for ( Season season : EnumSet.allOf( Season.class ) )
{
System.out.println( "Season: " + season.toString() + " = " + season.getMonths() );
}
}
}
Group
Represent each grouping of customers’ lawns/yards, (A, B, C, D, E) as an enum named Group
. Each of these enum objects holds a Map
, mapping a Season
enum object to a Set
of DayOfWeek
enum objects. For example, Group.A
in Season.SPRING
allows watering on two days, DayOfWeek.TUESDAY
& DayOfWeek.THURSDAY
.
package com.basilbourque.watering;
import java.time.DayOfWeek;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
public enum Group
{
A(
Map.of(
Season.SPRING , EnumSet.of( DayOfWeek.TUESDAY , DayOfWeek.THURSDAY ) ,
Season.SUMMER , EnumSet.allOf( DayOfWeek.class ) ,
Season.FALL , EnumSet.of( DayOfWeek.TUESDAY , DayOfWeek.THURSDAY ) ,
Season.WINTER , EnumSet.of( DayOfWeek.TUESDAY )
)
),
B(
Map.of(
Season.SPRING , EnumSet.of( DayOfWeek.FRIDAY ) ,
Season.SUMMER , EnumSet.allOf( DayOfWeek.class ) ,
Season.FALL , EnumSet.of( DayOfWeek.TUESDAY , DayOfWeek.FRIDAY ) ,
Season.WINTER , EnumSet.of( DayOfWeek.FRIDAY )
)
),
C(
Map.of(
Season.SPRING , EnumSet.of( DayOfWeek.MONDAY ) ,
Season.SUMMER , EnumSet.allOf( DayOfWeek.class ) ,
Season.FALL , EnumSet.of( DayOfWeek.MONDAY , DayOfWeek.TUESDAY ) ,
Season.WINTER , EnumSet.of( DayOfWeek.MONDAY )
)
),
D(
Map.of(
Season.SPRING , EnumSet.of( DayOfWeek.WEDNESDAY , DayOfWeek.FRIDAY ) ,
Season.SUMMER , EnumSet.allOf( DayOfWeek.class ) ,
Season.FALL , EnumSet.of( DayOfWeek.FRIDAY ) ,
Season.WINTER , EnumSet.of( DayOfWeek.WEDNESDAY )
)
),
E(
Map.of(
Season.SPRING , EnumSet.of( DayOfWeek.TUESDAY ) ,
Season.SUMMER , EnumSet.allOf( DayOfWeek.class ) ,
Season.FALL , EnumSet.of( DayOfWeek.TUESDAY , DayOfWeek.WEDNESDAY ) ,
Season.WINTER , EnumSet.of( DayOfWeek.WEDNESDAY )
)
);
private Map < Season, Set < DayOfWeek > > map;
// Constructor
Group ( Map < Season, Set < DayOfWeek > > mapArg )
{
this.map = mapArg;
}
// Getter
private Map < Season, Set < DayOfWeek > > getMapOfSeasonToDaysOfWeek() {
return this.map ;
}
// Retrieve the DayOfWeek set for this particular Group.
public Set<DayOfWeek> daysForSeason (Season season ) {
Set<DayOfWeek> days = this.map.get( season ) ; // Retrieve the value (set of days) for this key (a season) for this particular grouping of lawns/yards.
return days;
}
// Run `main` for demo/testing.
public static void main ( String[] args )
{
// Dump all these enum objects to console.
for ( Group group : EnumSet.allOf( Group.class ) )
{
System.out.println( "Group: " + group.toString() + " = " + group.getMapOfSeasonToDaysOfWeek() );
}
}
}
Schedule
Pull it all together in this Schedule
class.
This class makes use of the two enums defined above to get useful work done. The only method implemented so far tells you which days of the week are allowed for a particular group on a particular date. The method determines which Season
applies for that date.
Run the main
method here to dump the contents of our two enums, and report on the days of the week allowing for watering in each group for a particular hard-coded date.
package com.basilbourque.watering;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.IsoFields;
import java.util.EnumSet;
import java.util.Set;
public class Schedule
{
static private DateTimeFormatter isoWeekFormatter = DateTimeFormatter.ofPattern( "uuuu-'W'ww" ) ;
static public Set < DayOfWeek > daysForGroupOnDate ( Group group , LocalDate localDate )
{
Season season = Season.ofLocalDate( localDate );
Set < DayOfWeek > days = group.daysForSeason( season );
return days;
}
// Run `main` for demo/testing.
public static void main ( String[] args )
{
Season.main( null );
Group.main( null );
// Dump all these enum objects to console.
for ( Group group : EnumSet.allOf( Group.class ) )
{
LocalDate localDate = LocalDate.now( ZoneId.of( "Africa/Tunis" ) );
Set < DayOfWeek > days = Schedule.daysForGroupOnDate( group , localDate );
String week = localDate.format( Schedule.isoWeekFormatter ) ; // Standard ISO 8601 week, where week number one has the first Thursday of the calendar year, and week starts on Monday, so year is either 52 or 53 weeks long.
String message = "Group " + group + " – Watering days on " + localDate + " week # " + week + " is: " + days;
System.out.println( message );
}
}
}
Console
When running Schedule.main
, we see this dumped to the console.
Season: SPRING = [MARCH, APRIL]
Season: SUMMER = [MAY, JUNE, JULY, AUGUST]
Season: FALL = [SEPTEMBER, OCTOBER]
Season: WINTER = [NOVEMBER, DECEMBER, JANUARY, FEBRUARY]
Group: A = {SPRING=[TUESDAY, THURSDAY], FALL=[TUESDAY, THURSDAY], SUMMER=[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY], WINTER=[TUESDAY]}
Group: B = {SPRING=[FRIDAY], FALL=[TUESDAY, FRIDAY], SUMMER=[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY], WINTER=[FRIDAY]}
Group: C = {SPRING=[MONDAY], FALL=[MONDAY, TUESDAY], SUMMER=[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY], WINTER=[MONDAY]}
Group: D = {SPRING=[WEDNESDAY, FRIDAY], FALL=[FRIDAY], SUMMER=[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY], WINTER=[WEDNESDAY]}
Group: E = {SPRING=[TUESDAY], FALL=[TUESDAY, WEDNESDAY], SUMMER=[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY], WINTER=[WEDNESDAY]}
Group A – Watering days on 2018-01-30 week # 2018-W05 is: [TUESDAY]
Group B – Watering days on 2018-01-30 week # 2018-W05 is: [FRIDAY]
Group C – Watering days on 2018-01-30 week # 2018-W05 is: [MONDAY]
Group D – Watering days on 2018-01-30 week # 2018-W05 is: [WEDNESDAY]
Group E – Watering days on 2018-01-30 week # 2018-W05 is: [WEDNESDAY]
ISO 8601 week
You may find it helpful to learn about the ISO 8601 standard for a definition of week. The standard gives a specific meaning to “week” and defines a textual format for representing a particular week or a particular day within that week.
For working with such weeks within Java, consider adding the ThreeTen-Extra library to your project to make use of the YearWeek
class.
LocalDate
The LocalDate
class represents a date-only value without time-of-day and without time zone.
A time zone is crucial in determining a date. For any given moment, the date varies around the globe by zone. For example, a few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.
If no time zone is specified, the JVM implicitly applies its current default time zone. That default may change at any moment, so your results may vary. Better to specify your desired/expected time zone explicitly as an argument.
Specify a proper time zone name in the format of continent/region
, such as America/Montreal
, Africa/Casablanca
, or Pacific/Auckland
. Never use the 3-4 letter abbreviation such as EST
or IST
as they are not true time zones, not standardized, and not even unique(!).
ZoneId z = ZoneId.of( "America/Montreal" ) ;
LocalDate today = LocalDate.now( z ) ;
If you want to use the JVM’s current default time zone, ask for it and pass as an argument. If omitted, the JVM’s current default is applied implicitly. Better to be explicit.
ZoneId z = ZoneId.systemDefault() ; // Get JVM’s current default time zone.
Or specify a date. You may set the month by a number, with sane numbering 1-12 for January-December.
LocalDate ld = LocalDate.of( 1986 , 2 , 23 ) ; // Years use sane direct numbering (1986 means year 1986). Months use sane numbering, 1-12 for January-December.
Or, better, use the Month
enum objects pre-defined, one for each month of the year. Tip: Use these Month
objects throughout your codebase rather than a mere integer number to make your code more self-documenting, ensure valid values, and provide type-safety.
LocalDate ld = LocalDate.of( 1986 , Month.FEBRUARY , 23 ) ;
Immutable collections
The lists, sets, and maps seen above should ideally be immutable collections, as changing the membership of those collections is likely to be confounding and erroneous.
The new Java 9 syntax List.of
and Map.of
are already promised to be immutable. However, in our case the Map
should ideally be an EnumMap
for efficiency of performance and memory. The current implementation of Map.of
and Set.of
apparently do not detect the use of enums as members and automatically optimize with an internal use of EnumMap
and EnumSet
. There is a OpenJDK issue opened to consider such issues: consider enhancements to EnumMap and EnumSet.
One way to obtain an immutable EnumSet and immutable EnumMap is through the Google Guava library:
The results of each use an underlying EnumSet
/EnumMap
. Operations such as get
and put
throw an exception. So you get the enum-related optimizations along with immutability.
Here are the Season
and Group
classes seen above modified to use the Google Guava 23.6 library.
Season
with immutability
package com.basilbourque.watering;
import java.time.LocalDate;
import java.time.Month;
import java.util.EnumSet;
import java.util.List;
public enum Season
{
SPRING( List.of( Month.MARCH , Month.APRIL ) ), // `List.of` provides literals-style syntax, and returns an immutable `List`. New in Java 9.
SUMMER( List.of( Month.MAY , Month.JUNE, Month.JULY , Month.AUGUST ) ),
FALL( List.of( Month.SEPTEMBER , Month.OCTOBER ) ),
WINTER( List.of( Month.NOVEMBER , Month.DECEMBER , Month.JANUARY , Month.FEBRUARY ) );
private List< Month > months;
// Constructor
Season ( List < Month > monthsArg )
{
this.months = monthsArg;
}
public List < Month > getMonths ( )
{
return this.months;
}
// For any given month, determine the season.
static public Season ofLocalMonth ( Month monthArg )
{
Season s = null;
for ( Season season : EnumSet.allOf( Season.class ) )
{
if ( season.getMonths().contains( monthArg ) )
{
s = season;
break; // Bail out of this FOR loop.
}
}
return s;
}
// For any given date, determine the season.
static public Season ofLocalDate ( LocalDate localDateArg )
{
Month month = localDateArg.getMonth();
Season s = Season.ofLocalMonth( month );
return s;
}
// Run `main` for demo/testing.
public static void main ( String[] args )
{
// Dump all these enum objects to console.
for ( Season season : EnumSet.allOf( Season.class ) )
{
System.out.println( "Season: " + season.toString() + " = " + season.getMonths() );
}
}
}
Group
with immutability
package com.basilbourque.watering;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.time.DayOfWeek;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
public enum Group
{
A(
Maps.immutableEnumMap(
Map.of( // `Map.of` provides literals-style syntax, and returns an immutable `Map`. New in Java 9.
Season.SPRING , Sets.immutableEnumSet( DayOfWeek.TUESDAY , DayOfWeek.THURSDAY ) ,
Season.SUMMER , Sets.immutableEnumSet( EnumSet.allOf( DayOfWeek.class ) ) ,
Season.FALL , Sets.immutableEnumSet( DayOfWeek.TUESDAY , DayOfWeek.THURSDAY ) ,
Season.WINTER , Sets.immutableEnumSet( DayOfWeek.TUESDAY )
)
)
),
B(
Maps.immutableEnumMap(
Map.of(
Season.SPRING , Sets.immutableEnumSet( DayOfWeek.FRIDAY ) ,
Season.SUMMER , Sets.immutableEnumSet( EnumSet.allOf( DayOfWeek.class ) ) ,
Season.FALL , Sets.immutableEnumSet( DayOfWeek.TUESDAY , DayOfWeek.FRIDAY ) ,
Season.WINTER , Sets.immutableEnumSet( DayOfWeek.FRIDAY )
)
)
),
C(
Maps.immutableEnumMap(
Map.of(
Season.SPRING , Sets.immutableEnumSet( DayOfWeek.MONDAY ) ,
Season.SUMMER , Sets.immutableEnumSet( EnumSet.allOf( DayOfWeek.class ) ) ,
Season.FALL , Sets.immutableEnumSet( DayOfWeek.MONDAY , DayOfWeek.TUESDAY ) ,
Season.WINTER , Sets.immutableEnumSet( DayOfWeek.MONDAY )
)
)
),
D(
Maps.immutableEnumMap(
Map.of(
Season.SPRING , Sets.immutableEnumSet( DayOfWeek.WEDNESDAY , DayOfWeek.FRIDAY ) ,
Season.SUMMER , Sets.immutableEnumSet( EnumSet.allOf( DayOfWeek.class ) ) ,
Season.FALL , Sets.immutableEnumSet( DayOfWeek.FRIDAY ) ,
Season.WINTER , Sets.immutableEnumSet( DayOfWeek.WEDNESDAY )
)
)
),
E(
Maps.immutableEnumMap(
Map.of(
Season.SPRING , Sets.immutableEnumSet( DayOfWeek.TUESDAY ) ,
Season.SUMMER , Sets.immutableEnumSet( EnumSet.allOf( DayOfWeek.class ) ) ,
Season.FALL , Sets.immutableEnumSet( EnumSet.of( DayOfWeek.TUESDAY , DayOfWeek.WEDNESDAY ) ) ,
Season.WINTER , Sets.immutableEnumSet( DayOfWeek.WEDNESDAY )
)
)
);
private Map < Season, Set < DayOfWeek > > map;
// Constructor
Group ( Map < Season, Set < DayOfWeek > > mapArg )
{
this.map = mapArg;
}
// Getter
private Map < Season, Set < DayOfWeek > > getMapOfSeasonToDaysOfWeek ( )
{
return this.map;
}
// Retrieve the DayOfWeek set for this particular Group.
public Set < DayOfWeek > daysForSeason ( Season season )
{
Set < DayOfWeek > days = this.map.get( season ); // Retrieve the value (set of days) for this key (a season) for this particular grouping of lawns/yards.
return days;
}
// Run `main` for demo/testing.
public static void main ( String[] args )
{
// Dump all these enum objects to console.
for ( Group group : EnumSet.allOf( Group.class ) )
{
System.out.println( "Group: " + group.toString() + " = " + group.getMapOfSeasonToDaysOfWeek() );
}
}
}
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
Where to obtain the java.time classes?
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.