Issue
I have this fonction:
/**
* @return From november to december -> current year +1 else current year
*/
public static int getCurrentScolYear () {
int month = Calendar.getInstance().get(Calendar.MONTH);
if ( month == NOVEMBER || month == DECEMBER) {
return Calendar.getInstance().get(Calendar.YEAR) +1;
}
return Calendar.getInstance().get(Calendar.YEAR);
}
This is used to calculate a scolar year (that start in november).
I would like to test this in a Junit test, by changing the 'current time'. So I can test the result for different dates.
I find this thread but it seems over complicated: java: how to mock Calendar.getInstance()?
Is there a simple solution to test this?
Solution
java.time
The terrible Calendar
class was supplanted years ago by the java.time classes, specifically ZonedDateTime
. There is no reason to ever using Calendar
again.
Below is my replacement for your code.
Custom class
We could return the type-safe and self-explanatory java.time.Year
object rather than a mere integer number. But that would be misleading as that class is explicitly defined as an ISO 8601 compliant year number.
I suggest instead you define your own AcademicYear
class. Pass around objects of this class to make your code more self-documenting, provide for type-safety, and ensure valid values. Something like the following class.
This custom class also provides a home for your static methods.
This class follows the lead of the java.time.Year
class, including its naming conventions.
An academic year is actually two year numbers, the starting year and the following stopping year. The class below stores both.
Example usages:
AcademicYear.now().getDisplayName() // 2017-2018
or
AcademicYear.now().getValueStart() // First year of the two-year range, such as 2018 of the year 2018-2019.
I threw this class together without testing. So use this as a guide, not production code.
AcademicYear.java
package com.basilbourque.example;
import java.time.*;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
// This class follows the lead of the `java.time.Year` class.
// See JavaDoc: https://docs.oracle.com/javase/10/docs/api/java/time/Year.html
// See OpenJDK source-code: http://hg.openjdk.java.net/jdk10/master/file/be620a591379/src/java.base/share/classes/java/time/Year.java
// This class follows the immutable objects pattern. So only getter accessor methods, no setters.
public class AcademicYear {
// Statics
static public int FIRST_YEAR_LIMIT = 2017;
static public int FUTURE_YEARS_LIMIT = 10;
static public Set < Month > academicYearStartingMonths = EnumSet.of( Month.NOVEMBER , Month.DECEMBER );
// Members
private int academicYearNumberStart;
// Constructor
private AcademicYear ( int academicYearNumberStart ) {
if ( academicYearNumberStart < AcademicYear.FIRST_YEAR_LIMIT ) {
throw new IllegalArgumentException( "Received a year number: " + academicYearNumberStart + " that is too long ago (before " + AcademicYear.FIRST_YEAR_LIMIT + "). Message # c5fd65c1-ed10-4fa1-96db-77d08ef1d97e." );
}
if ( academicYearNumberStart > Year.now().getValue() + AcademicYear.FUTURE_YEARS_LIMIT ) {
throw new IllegalArgumentException( "Received a year number that is too far in the future, over " + AcademicYear.FUTURE_YEARS_LIMIT + " away. Message # 8581e4ca-afb3-4ab7-8849-9b02c434eb4c." );
}
this.academicYearNumberStart = academicYearNumberStart;
}
public static AcademicYear of ( int academicYearNumberStart ) {
return new AcademicYear( academicYearNumberStart );
}
public int getValueStart ( ) {
return this.academicYearNumberStart;
}
public int getValueStop ( ) {
return ( this.academicYearNumberStart + 1 );
}
public String getDisplayName ( ) {
String s = this.academicYearNumberStart + "-" + ( this.academicYearNumberStart + 1 );
return s;
}
// ------| `Object` |---------------------
@Override
public String toString ( ) {
return "AcademicYear{ " +
"academicYearNumberStart=" + academicYearNumberStart +
" }";
}
@Override
public boolean equals ( Object o ) {
if ( this == o ) return true;
if ( o == null || getClass() != o.getClass() ) return false;
AcademicYear that = ( AcademicYear ) o;
return this.getDisplayName().equals( that.getDisplayName() );
}
@Override
public int hashCode ( ) {
return Objects.hash( this.getDisplayName() );
}
// -----------| Factory methods |-------------------
static public AcademicYear now ( ) { // I think making ZoneId optional is a poor design choice, but I do so here to mimic `java.time.Year`.
AcademicYear ay = AcademicYear.now( Clock.systemDefaultZone() );
return ay;
}
static public AcademicYear now ( ZoneId zoneId ) {
// Determine the current date as seen in the wall-clock time used by the people of a particular region (a time zone).
AcademicYear ay = AcademicYear.now( Clock.system( zoneId ) );
return ay;
}
static public AcademicYear now ( Clock clock ) {
final LocalDate localDate = LocalDate.now( clock );
AcademicYear ay = AcademicYear.from( localDate );
return ay;
}
static public AcademicYear from ( LocalDate localDate ) {
Objects.requireNonNull( localDate , "Received a null `LocalDate` object. Message # 558dd5e8-5cff-4c6e-b0f8-40dbcd76a753." );
// Extract the month of the specified date. If not Nov or Dec, subtract one from the year.
int y = localDate.getYear();
// If not November or December, subtract 1.
int startingYear;
if ( ! academicYearStartingMonths.contains( localDate.getMonth() ) ) {
startingYear = ( y - 1 );
} else {
startingYear = y;
}
AcademicYear ay = AcademicYear.of( startingYear );
return ay;
}
}
ZoneId
Another change is the required argument for ZoneId
, a time zone.
A time zone is crucial in determining a date, and therefore a year. 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 during runtime(!), 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 2-4 letter abbreviation such as EST
or IST
as they are not true time zones, not standardized, and not even unique(!).
Break up methods
Another change, is we have two sets of methods rather than one single method.
One method, AcademicYear.from
, has responsibility for the logic of making the November adjustment. You pass a LocalDate
to this method.
The other methods, three variations of AcademicYear.now
, are responsible for determining the current moment, adjusting to the specified time zone, and extracting a date-only LocalDate
object. That LocalDate
object is then passed to the other method.
Clock
for unit-testing
For testing, you can pass to one of the now
method variations a Clock
instance that alters time as you may desire. The Clock
class provides several clocks with altered behavior, such as fixed-point in time, current moment plus/minus a span-of-time, and altered cadence where the clock ticks in your specified granularity such as every 5 minutes.
For more info on the altered Clock
behaviors, see my Answer to another Question.
Example:
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ; // 2018-01-23
LocalTime lt = LocalTime.of( 8 , 0 ) ; // 8 AM.
Instant instant = ZonedDateTime.of( ld , lt , z ).toInstant() ;
Clock c = Clock.fixed( instant , z ) ;
AcademicYear ay = AcademicYear.now( c ) ;
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.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
- Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
- Java 9 adds some minor features and fixes.
- Java SE 6 and Java SE 7
- Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
- Android
- Later versions of Android bundle implementations of the java.time classes.
- For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
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.
Answered By - Basil Bourque