Search code examples
clojureclojure-java-interop

Looking for a way to partition date on a monthly basis in Clojure


Want to split a date range into monthly chunks.

example Input - [10/20/2019 - 12/20/2019]

example output - { [10/20/2019 10/31/2019] [11/01/2019 11/30/2019] [12/012019 12/20/2019] }

Thank you


Solution

  • here is a simple draft of what you can do (with java interop, no external libs):

    first of all let's make an iteration by month, starting with the specified one:

    (defn by-month [[mm yyyy]]
      (iterate #(.plusMonths % 1)
               (java.time.YearMonth/of yyyy mm))) 
    
    user> (take 4 (by-month [10 2019]))
    ;;=> (#object[java.time.YearMonth 0x62fc8302 "2019-10"]
    ;;    #object[java.time.YearMonth 0x1a7bc211 "2019-11"]
    ;;    #object[java.time.YearMonth 0x6a466e83 "2019-12"]
    ;;    #object[java.time.YearMonth 0x652ac30f "2020-01"])
    

    then get start and end date for each YearMonth:

    (defn start-end [^java.time.YearMonth ym]
      [(.atDay ym 1) (.atEndOfMonth ym)])
    
    ;;=> ([#object[java.time.LocalDate 0xe880abb "2019-10-01"]
    ;;     #object[java.time.LocalDate 0x54aadf50 "2019-10-31"]]
    ;;    [#object[java.time.LocalDate 0x14c1b42d "2019-11-01"]
    ;;     #object[java.time.LocalDate 0x32d0a22c "2019-11-30"]])
    

    now, wrap it up in a function of ranges by your input dates:

    (defn day-range [[mm1 dd1 yyyy1] [mm2 dd2 yyyy2]]
      (let [start (java.time.LocalDate/of yyyy1 mm1 dd1)
            end (java.time.LocalDate/of yyyy2 mm2 dd2)
            internal (->> [mm1 yyyy1]
                          by-month
                          (mapcat start-end)                     
                          (drop 1)
                          (take-while #(neg? (compare % end))))]
        (partition 2 `(~start ~@internal ~end))))
    
    user> (day-range [10 20 2019] [12 20 2019])
    ;;=> ((#object[java.time.LocalDate 0x6a8f92f2 "2019-10-20"]
    ;;     #object[java.time.LocalDate 0x10135df3 "2019-10-31"])
    ;;    (#object[java.time.LocalDate 0x576bcff7 "2019-11-01"]
    ;;     #object[java.time.LocalDate 0x7b5ed908 "2019-11-30"])
    ;;    (#object[java.time.LocalDate 0x6b2117a9 "2019-12-01"]
    ;;     #object[java.time.LocalDate 0x57bf0864 "2019-12-20"]))
    

    now you can postprocess each start-end pair as you need:

    (map (fn [[^java.time.LocalDate start ^java.time.LocalDate end]]
           (let [fmt (java.time.format.DateTimeFormatter/ofPattern "MM/dd/yyyy")]
             [(.format start fmt) (.format end fmt)]))
         (day-range [10 20 2019] [12 20 2019]))
    
    ;;=> (["10/20/2019" "10/31/2019"]
    ;;    ["11/01/2019" "11/30/2019"]
    ;;    ["12/01/2019" "12/20/2019"])
    

    another way is to iterate by day, and then partition by [month year], collecting first-last afterwards:

    (defn ranges [[mm1 dd1 yyyy1] [mm2 dd2 yyyy2]]
      (let [start (java.time.LocalDate/of yyyy1 mm1 dd1)
            end (java.time.LocalDate/of yyyy2 mm2 dd2)]
        (->> start
             (iterate (fn [^java.time.LocalDate curr] (.plusDays curr 1)))         
             (take-while (fn [^java.time.LocalDate dt] (not (pos? (compare dt end)))))
             (partition-by (fn [^java.time.LocalDate dt] [(.getMonthValue dt) (.getYear dt)]))
             (map (juxt first last)))))
    
    user> (ranges [10 20 2019] [12 20 2019])
    ;;=> ([#object[java.time.LocalDate 0x383f6a9e "2019-10-20"]
    ;;     #object[java.time.LocalDate 0x2ca14c39 "2019-10-31"]]
    ;;    [#object[java.time.LocalDate 0x74d1974 "2019-11-01"]
    ;;     #object[java.time.LocalDate 0x5f6c16cc "2019-11-30"]]
    ;;    [#object[java.time.LocalDate 0x74f63a42 "2019-12-01"]
    ;;     #object[java.time.LocalDate 0x4b90c388 "2019-12-20"]])
    

    which produces some unneeded intermediate vals, but also gives you a way to split ranges however you want.