Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Representing just 'year' and 'year-month' in Clojure/Java

Tags:

java

clojure

I am trying to represent time information that has precision at the scale of year or year-month. Eg. 'patient has a history of fracture of the right wrist in 1998'; 'history of headaches since October 2015'.

I looked at Joda-Time which has YearMonth org.joda.time.YearMonth but not just 'Year'. But even the YearMonth seems kinda a disconnected. E.g.

(clj-time.core/year-month 2015 02)
(clj-time.core/date-time  2015 11 15)

=> #object[org.joda.time.YearMonth 0x7a19d121 "2015-02"]
=> #object[org.joda.time.DateTime 0x5625de93 "2015-11-15T00:00:00.000Z"]

and you can do some operations such as:

(clj-time.core/month (clj-time.core/year-month 2015 02))
(clj-time.core/month (clj-time.core/date-time  2015 11 15))

=> 2
=> 11

but not this:

(clj-time.core/before? (clj-time.core/year-month 2015 02)
                       (clj-time.core/date-time  2015 11 15))

=> Exception thrown: java.lang.ClassCastException (org.joda.time.DateTime 
                     cannot be cast to org.joda.time.ReadablePartial)

and similar results with java.time:

(.compareTo (java.time.Year/of 2007) (java.time.LocalDate/now))

=> Exception thrown: java.lang.ClassCastException (java.time.LocalDate
                     cannot be cast to java.time.Year)

What I want to be able to do is represent temporal entities such as Year and YearMonth at the precision they are - not more, not less, and also do date math with them. E.g.:

  • The year 1998 would be represented as an interval '01 Jan 1998' to '31 Dec 1998' and October 2015 as '01 October 2015' to ' 31 October 2015'.
  • And be able to compare and do other operations on these temporal entities.

Has anyone found a good solution for this? Or should I look at building my own solution/library?

like image 494
midoc Avatar asked Jul 19 '26 09:07

midoc


1 Answers

[Java syntax rather than Clojure.)

Java 8+

Java 8 and later comes with an industry-leading date-time framework, the java.time classes. These came from JSR 310, led by the same man who created Joda-Time, Stephen Colebourne.

java.time.Year

The class java.time.Year represents an entire year.

Year year = Year.of( 2015 ) ;

As for math, that class offers plus and minus methods.

Year followingYear = year.plusYears( 1 ) ;

java.time.LocalDate

If you want dates within the year, call various methods that return a java.time.LocalDate.

LocalDate firstOfYear = year.atDay( 1 ) ; // January 1st of that year.
LocalDate endOfYear = year.atDay( year.length() ) ;  // December 31st of that year. Year may be either 365 or 366 days long.

Custom class

If you want the pair of first & last days of year, just write your own little class.

import java.time.*;

public class YearBeginEnd
{
    public final Year year;
    public final LocalDate begin, end;
    public final int length;

    public YearBeginEnd ( final Year year )
    {
        this.year = year;
        this.length = this.year.length();
        this.begin = this.year.atDay( 1 );
        this.end = this.year.atDay( this.year.length() );
    }
}

java.time.MonthDay

If you want a day and month of that year, use MonthDay class.

MonthDay march17 = MonthDay.of( Month.MARCH , 17 ) ;
LocalDate march17Of2015 = year.atMonthDay( march17 ) ;

java.time.YearMonth

For year-month, use YearMonth class. You can get dates and do math similar to above.

YearMonth yearMonth = YearMonth.of( 2015 , Month.FEBRUARY ) 
YearMonth followingYearMonth = yearMonth.plusMonths( 1 ) ;
LocalDate start = yearMonth.atDay( 1 ) ;
LocalDate end = yearMonth.atEndOfMonth() ;

ThreeTen-Extra library

If you want other chunks of time, such as half-year or quarters, add the ThreeTen-Extra library to your project for more classes compatible with java.time.

That library also provides the LocalDateRange class which you could use as part of (or instead of) that custom class above.

import org.threeten.extra.LocalDateRange;

import java.time.LocalDate;
import java.time.Year;

public class YearBeginEnd
{
    public final Year year;
    public final LocalDateRange dateRangeClosed;
    public final int length;

    public YearBeginEnd ( final Year year )
    {
        this.year = year;
        this.length = this.year.length();
        this.dateRangeClosed = LocalDateRange.ofClosed( this.year.atDay( 1 ) , this.year.atDay( this.year.length() ) );
    }

    public LocalDate begin ( ) { return this.dateRange.getStart(); }
    public LocalDate end ( ) { return this.dateRange.getEnd(); }
}

Or represent that entire month.

LocalDateRange monthRangeClosed = 
    LocalDateRange.ofClosed( 
        YearMonth.of( 2015 , Month.FEBRUARY ).atDay( 1 ) ,
        YearMonth.of( 2015 , Month.FEBRUARY ).atEndOfMonth()
    );

Note that in date-time handling, it is generally wiser to use Half-Open rather than Fully-Closed approach. In Half-Open, the beginning of a span of time is inclusive while the ending is exclusive. So a month starts on day one, and runs up to, but does not include, the first of the following month.

LocalDateRange monthRange = 
    LocalDateRange.of( 
        YearMonth.of( 2015 , Month.FEBRUARY ).atDay( 1 ) ,
        YearMonth.of( 2015 , Month.FEBRUARY ).plusMonths( 1 ).atDay( 1 ) ;
    );

Notice that the LocalDateRange class offers handy methods such as abuts, overlaps, intersection, etc.

like image 61
Basil Bourque Avatar answered Jul 21 '26 21:07

Basil Bourque



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!