Extending clj-time

161 views
Skip to first unread message

j...@afandian.com

unread,
Sep 9, 2014, 1:18:28 PM9/9/14
to clo...@googlegroups.com
I have a type that's an extended date-time. It can be constructed with the usual year, month, day parameters but (for legacy reasons) the month can be overloaded to mean extra things. E.g. a month of 21 means 'spring', 33 means 'third quarter'. I want to construct this type in a way that it preserves that information (so, for example, it can print itself appropriately), but that has comparison and equality with other dates.

I want to extend clj-time somehow. My first thought is write a type that construct with the given arguments and implements the clj-time/DateTimeProtocol, converting whenever required (I don't need high performance).

(defprotocol IWeirdDate
  (as-date [this])
  (pp [this]))

(defrecord WeirdDate [year month day]
  IWeirdDate
  (pp [this] (condp = month
               21 (str "Spring " year)
               33 (str "Third Quarter of " year)
               (str (as-date this))))
  
  (as-date [this]
           (let [[y m d] (condp = month
                            21 [year 3 day]
                            33 [year 7 1] ; third quarter

                            ; ... others
                            
                            ; default
                            [year month day])]
                (clj-time/date-time y m d)))
  
  clj-time/DateTimeProtocol
  (clj-time/year [this] (clj-time/year (as-date this)))
  (clj-time/month [this] (clj-time/month (as-date this)))
  (clj-time/day [this] (clj-time/day (as-date this)))
  ; ... and the rest of DateTimeProtocol
)

Is this the right way to do this? Could I somehow make this implicit and avoid re-writing the DateTimeProtocol implementations?

Cheers

Joe

Michael Klishin

unread,
Sep 10, 2014, 8:39:21 AM9/10/14
to clo...@googlegroups.com, j...@afandian.com
On 10 September 2014 at 15:42:01, j...@afandian.com (j...@afandian.com) wrote:
> Is this the right way to do this?

Yes.

> Could I somehow make this implicit
> and avoid re-writing the DateTimeProtocol implementations?

There's no way around implementing the DateTimeProtocol functions you need for your
data structure but you can make some of your function delegate to the original
clj-time ones (e.g. by instantiating a DateTime or another class and passing
them on to clj-time). 
--
@michaelklishin, github.com/michaelklishin

j...@afandian.com

unread,
Sep 10, 2014, 11:15:51 AM9/10/14
to clo...@googlegroups.com, j...@afandian.com
Thanks. Sorry this is turning into a bit of a brain dump. I've created my record that decorates the original type:

(defprotocol IWeirdDate
  (as-date [this])
  (pp [this]))

(defrecord WeirdDate [year month day]
  IWeirdDate
  (pp [this] (condp = month
               21 (str "Spring " year)
               33 (str "Third Quarter of " year)
               (str (as-date this))
               ))
  
  (as-date [this]
           (let [[y m d] (condp = month
                            21 [year 3 day] ; spring
                            33 [year 7 1] ; third quarter

                            ; ... others
                            
                            ; default
                            [year month day])]
                (clj-time/date-time y m d)))
  
    clj-time/DateTimeProtocol
    (clj-time/year [this] (clj-time/year (as-date this)))
    (month [this] (clj-time/month (as-date this)))
    (day [this] (clj-time/day (as-date this)))
    (day-of-week [this] (clj-time/day-of-week (as-date this)))
    (hour [this] (clj-time/hour (as-date this)))
    (minute [this] (clj-time/minute (as-date this)))
    (sec [this] (clj-time/sec (as-date this)))
    (second [this] (clj-time/second (as-date this)))
    (milli [this] (clj-time/milli (as-date this)))
    (clj-time/after? [this that] (clj-time/after? (as-date this) that))
    (before? [this that] (clj-time/before? (as-date this) that)))

And I want to be able to use some functions in both directions:

(def x (WeirdDate. 1986 5 2))
(def y (clj-time/date-time 2014 5 2))
(prn (clj-time/before? x y))
(prn (clj-time/after? y x))

The first one works, the second one fails with `ClassCastException: WeirdDate cannot be cast to org.joda.time.ReadableInstant`. I think this is because of a type hint which is present on the on the org.joda.time.DateTime implementation[0] of the DateTimeProtocol but not on the protocol:

(after? [this ^ReadableInstant that] (.isAfter this that))

So I add the ReadableInstant methods to the WeirdDate record:

    ReadableInstant
    (.equals [this readableInstant] (.equals (as-date this) readableInstant))
    (.get [this type] (get (as-date this) type))
    (.getChronology [this] (.getChronology (as-date this)))
    (.getMillis [this] (.getMillis (as-date this)))
    (.getZone [this] (.getZone (as-date this)))
    (.hashCode [this] (.hashCode (as-date this)))
    (.isAfter [this instant] (.isAfter (as-date this) instant))
    (.isBefore [this instant] (.isBefore (as-date this) instant))
    (.isEqual [this instant] (.isEqual (as-date this) instant))
    (.isSupported [this field] (.isSupported (as-date this) field))
    (.toInstant [this] (.toInstant (as-date this)))
    (.toString [this] (.toString (as-date this)))

Now I get `IllegalArgumentException: Must hint overloaded method: get`. So I annotate according to the Java interface[1]:

    ReadableInstant
    (.equals ^boolean  [this ^Object readableInstant] (.equals (as-date this) readableInstant))
    (.get ^int [this ^DateTimeFieldType type] (get (as-date this) type))
    (.getChronology ^Chronology [this] (.getChronology (as-date this)))
    (.getMillis ^long [this] (.getMillis (as-date this)))
    (.getZone ^DateTimeZone [this] (.getZone (as-date this)))
    (.hashCode ^int [this] (.hashCode (as-date this)))
    (.isAfter ^boolean [this ^ReadableInstant instant] (.isAfter (as-date this) instant))
    (.isBefore ^boolean [this ^ReadableInstant instant] (.isBefore (as-date this) instant))
    (.isEqual ^boolean [this ^ReadableInstant instant] (.isEqual (as-date this) instant))
    (.isSupported ^boolean [this ^DateTimeFieldType field] (.isSupported (as-date this) field))
    (.toInstant ^Instant [this] (.toInstant (as-date this)))
    (.toString ^String [this] (.toString (as-date this)))

Again, `IllegalArgumentException: Must hint overloaded method: get`.

As far as I know this is all correctly annotated, although I'm not sure about the return types, as records aren't mentioned in the documentation[2] but it does say "For function return values, the type hint can be placed before the arguments vector".

Any ideas?

[2] http://clojure.org/java_interop#Java Interop-Type Hints

j...@afandian.com

unread,
Sep 10, 2014, 1:10:57 PM9/10/14
to clo...@googlegroups.com, j...@afandian.com
I just noticed that the ReadableInstant[0] interface is generic, extending Comparable<T> [1]. Is it possible to implement a generic interface with a defrecord?

Michael Klishin

unread,
Sep 10, 2014, 3:00:45 PM9/10/14
to clo...@googlegroups.com, j...@afandian.com
On 10 September 2014 at 21:11:06, j...@afandian.com (j...@afandian.com) wrote:
> I just noticed that the ReadableInstant[0] interface is generic,
> extending Comparable [1]. Is it possible to implement a generic
> interface with a defrecord?

Type parameters in generics do not exist at runtime, they are purely a javac
feature. So yes, you can reify a generic interface, just ignore the type variable(s). 
--
@michaelklishin, github.com/michaelklishin
Reply all
Reply to author
Forward
0 new messages