Skip to content

Instantly share code, notes, and snippets.

@ericnormand
Last active September 18, 2020 15:14
Show Gist options
  • Save ericnormand/f43768bac0024936e0274cfd2ce06aae to your computer and use it in GitHub Desktop.
Save ericnormand/f43768bac0024936e0274cfd2ce06aae to your computer and use it in GitHub Desktop.
395 PurelyFunctional.tv Newsletter

Seasons

Well, it's Hurricane Season here in the Gulf South. But that's not the kind of seasons I'm talking about now.

Your job is to take a month (keyword) day (number) and a hemisphere (:north or :south) and determine which season it is (return a keyword), according to this handy table.

Start       End         North  South
March 1     May 31      Spring Autumn
June 1      August 31   Summer Winter
September 1 November 30 Autumn Spring
December 1  February 29 Winter Summer

Example:

(which-season :north :march 5) ;=> :spring
(which-season :south :december 25) ;=> :summer

Thanks to this site for the challenge idea where it is considered Hard level in JavaScript.

Please submit your solutions as comments to this gist. Discussion is welcome.

@cloojure
Copy link

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [schema.core :as s]))

(def month-season-north
  "The correspondence between month of year and season for the northern hemisphere"
  [[:january :winter]
   [:february :winter]
   [:march :spring]
   [:april :spring]
   [:may :spring]
   [:june :summer]
   [:july :summer]
   [:august :summer]
   [:september :fall]
   [:october :fall]
   [:november :fall]
   [:december :winter]])

(def month-season-south
  "The correspondence between month of year and season for the northern hemisphere"
  (let [months        (mapv xfirst month-season-north)
        seasons-north (mapv xsecond month-season-north)
        seasons-south (it-> seasons-north
                        (cycle it)
                        (drop 6 it)
                        (take 12 it)
                        (vec it))
        result        (zip months seasons-south)]
    result))

(def month-season-lookup
  {:north  (into {} month-season-north)
   :south  (into {} month-season-south)})

(s/defn month->season  :- s/Keyword
  [hemisphere :- s/Keyword
   month  :- s/Keyword
   & junk]  ; day is not needed
  (fetch-in month-season-lookup [hemisphere month]))

(dotest
  (is= (month->season :north :march 5) :spring)
  (is= (month->season :south :december 25) :summer))

@KingCode
Copy link

KingCode commented Sep 15, 2020

The most natural way (to me) to structure the data is to index the seasons and reshape prior to use. Also, since I spent time worrying about day accuracy (which was buggy anyway), why not spice it up a little? Adding update-seasons and a signature to which-season.
(New bugfix: both 'start and 'end can be the same month, switched from condp to cond->, my first use thereof :)

(def seasons (->> {:spring {:north [[:march 1] [:may 31]]
                            :south [[:september 1] [:november 30]]}
                   :summer {:north [[:june 1] [:august 31]]
                            :south [[:december 1] [:february 29]]}
                   :autumn {:north [[:september 1] [:november 30]]
                            :south [[:march 1] [:may 31]]}
                   :winter {:north [[:december 1] [:february 29]]
                            :south [[:june 1] [:august 31]]}}))

(defn reshape [seasons]
  (->> seasons
       (map (fn [[season {:keys [north south]}]]
              {[:north north] season
               [:south south] season}))
       (apply merge)))

(defn update-seasons [seasons [[season hemi start-date end-date :as change] & more]]
  (if-not change 
    seasons
    (update-seasons (assoc-in seasons 
                              [season hemi] 
                              [start-date end-date]) 
                    more)))

(def months (cycle [:january :february :march :april :may :june
                    :july :august :september :november :december]))

(def month-days (zipmap months [31 29 31 30 31 30 31 30 30 31 30 31]))

(defn month-in-period? [month start-month end-month]
  (let [period (->> months 
                    (drop-while (complement #{start-month}))
                    (take-while (complement #{end-month}))
                    (into [])
                    (#(conj % end-month)))]
    (some #{month} period)))

(defn which-season 
  ([h m d]
   (which-season seasons h m d))
  ([seasons hemi m d]
   (let [seasons (reshape seasons)
         xf (comp (filter (fn [[h _]]
                            (= hemi h)))
                  (filter (fn [[_ [[start-month _] [end-month _]]]]
                            (month-in-period? m start-month end-month)))
                  (filter (fn [[_ [[start-month start-day] [end-month end-day]]]]
                            (cond-> true ;; bugfix again 
                              (= m start-month) (and (<= start-day d 
                                                         (month-days start-month)))
                              (= m end-month) (and (<= 1 d end-day))
                              :else (and (<= 1 d (month-days m)))))))
         key (->> seasons keys (sequence xf) first)]
     (get seasons key "Not Found"))))

(let [updated (update-seasons seasons 
                              [[:spring :north [:march 21] [:june 20]]
                               [:winter :north [:december 1] [:march 20]]
                               [:summer :north [:june 21] [:august 31]]])]
  (->> '(:march 15 :june 15 :june 29 :february 5 :september 10) (partition 2)
       (map #(mapv (fn [h] (apply which-season updated h %)) 
                   [:north :south])))) 

;; => ([:winter :autumn] [:spring :winter] [:summer :winter] [:winter :summer] [:autumn :spring])

@daveschoutens
Copy link

daveschoutens commented Sep 15, 2020

Because the problem talks about meteorological seasons, the date thing is just a distraction. I went with this:

(defn which-season [hemisphere month]
  (let [seasons [{:north :winter :south :summer :months #{:december :january :february}}
                 {:north :spring :south :autumn :months #{:march :april :may}}
                 {:north :summer :south :winter :months #{:june :july :august}}
                 {:north :autumn :south :spring :months #{:september :october :november}}]]
    (->> seasons 
         (filter #(contains? (:months %) month)) 
         (map hemisphere) 
         first)))

Also @mchampine:

Btw, here's a little hack to get month name keywords

How about this?

(->> (java.time.Month/values) 
     (map #(clojure.string/lower-case (.name %)))
     (map keyword))
;; (:january :february :march :april :may :june :july :august :september :october :november :december)

@mchampine
Copy link

mchampine commented Sep 15, 2020

Also @mchampine:
How about this?

Nice! That's a cleaner way of getting all the month names. And since we're code golfing, how about:

(map (comp keyword #(clojure.string/lower-case (.name %))) (java.time.Month/values))
;; (:january :february :march :april :may :june :july :august :september :october :november :december)

For my use I'd just have to rotate the months to start with december, something like:

#(cons (last %) (drop-last %))

;; or more generically
(defn rotate-by [n s]
  (let [cs (count s)]
    (take cs (drop (mod n cs) (cycle s)))))

#(rotate-by -1 %)

(p.s. I looked over the java.time.Month javadoc, wondering if the order of enum values returned is guaranteed. Yup, values() will always return them in the order they're declared. Good, that order is unlikely to change! It also says .toString is preferred over .name)

@claj
Copy link

claj commented Sep 15, 2020

ugly but works

(def northern
  {:january :winter,
   :february :winter,
   :march :spring,
   :april :spring,
   :may :spring,
   :june :summer,
   :july :summer,
   :august :summer
   :september :fall,
   :october :fall,
   :november :fall,
   :december :winter})

(def reverse-season
 {:winter :summer
  :summer :winter
  :spring :autumn
  :autumn :spring})

(defn which-season [hemi month _date]
  (cond-> (northern month)
    (#{:south} hemi) reverse-season))

@dfuenzalida
Copy link

dfuenzalida commented Sep 16, 2020

This solution parses the text and also takes care of the days at the beginning of the year, which are tricky:

(ns season.core
  (:require [clojure.string :refer [lower-case]]
            [clojure.edn :as edn]))

(def input
  "Start       End         North  South
   March 1     May 31      Spring Autumn
   June 1      August 31   Summer Winter
   September 1 November 30 Autumn Spring
   December 1  February 29 Winter Summer")

(def months
  (apply array-map
         [:january 31 :february 29 :march 31 :april 30 :may 31 :june 30
          :july 31 :august 31 :september 30 :october 31 :november 30 :december 31]))

(defn day-of-year [month day-of-month]
  (let [days  (->> (take-while #(not= month %) (keys months))
                   (map months))]
    (reduce + day-of-month days)))

(def keywordize
  (comp keyword lower-case))

(defn normalize [start end]
  (let [end (if (> start end) (+ end 365) end)]
    [start end]))

(defn make-range [[month1 day1 month2 day2 season1 season2]]
  (let [start   (day-of-year (keywordize month1) (edn/read-string day1))
        end     (day-of-year (keywordize month2) (edn/read-string day2))
        seasons {:north (keywordize season1) :south (keywordize season2)}]
    [(normalize start end) seasons]))

(def make-ranges-table
  (delay (->> (re-seq #"\w+" input)
              (drop 4)
              (partition 6)
              (map make-range))))

(defn which-season* [hemi month day]
  (let [doy (day-of-year month day)
        row (first
             (filter (fn [[[lo hi] _]] (<= lo doy hi))
                     @make-ranges-table))
        [_ seasons] row]
    (get seasons hemi)))

(defn which-season [hemi month day]
  (or (which-season* hemi month day)
      (which-season* hemi month (+ 365 day))))

(comment
  (which-season :north :march 5) ;=> :spring
  (which-season :south :december 25) ;=> :summer
  (which-season :south :january 2) ;=> :summer
  )

@emeraldimp
Copy link

A simpler F# version:

    type Hemisphere = North | South
    
    type Month = January | February | March | April | May | June | July | August | September | October | November | December
    
    type Season = Spring | Summer | Autumn | Winter

    let whichSeason hemisphere month day =
        let invertSeason season =
            match season with
            | Spring -> Autumn
            | Summer -> Winter
            | Autumn -> Spring
            | Winter -> Summer

        let northernSeason month =
            match month with
            | March | April | May -> Spring
            | June | July | August -> Summer
            | September | October | November -> Autumn
            | December | January | February -> Winter
        
        match hemisphere with
            | North -> northernSeason month
            | South -> invertSeason (northernSeason month)

@msladecek
Copy link

msladecek commented Sep 18, 2020

I first started with a nested map with month and then hemisphere for keys.
Then I wanted to extend the solution to also cover astronomical seasons, while keeping the (get-in seasons [date hemisphere]) pattern.

Here it goes

(def month->int (zipmap [:january :february :march :april :may :june :july :august :september :october :november :december]
                        (range)))

(defn date<= [& dates]
  (->> dates
       (map (fn [[month day]] [(month->int month) day]))
       (partition 2 1)
       (map #(apply compare %))
       (every? #(<= % 0))))

(def seasons-astronomical
  (reify clojure.lang.ILookup
    (valAt [this date]
      (cond
        (date<= [:march     21] date [:june      20]) {:north :spring :south :autumn}
        (date<= [:june      21] date [:september 22]) {:north :summer :south :winter}
        (date<= [:september 23] date [:december  20]) {:north :autumn :south :spring}
        (or (date<= [:december 21] date [:december 31])
            (date<= [:january   1] date [:march    20])) {:north :winter :south :summer}))))


(def seasons-meteorological
  (reify clojure.lang.ILookup
    (valAt [this [month _]]
      (let [by-season {#{:march     :april   :may}      {:north :spring :south :autumn}
                       #{:june      :july    :august}   {:north :summer :south :winter}
                       #{:september :october :november} {:north :autumn :south :spring}
                       #{:december  :january :february} {:north :winter :south :summer}}
            by-month (into {}
                           (mapcat
                            (fn [[months by-hemisphere]]
                              (for [month months]
                                [month by-hemisphere]))
                            by-season))]
        (by-month month)))))

(defn which-season
  ([hemisphere month day]
   (get-in seasons-meteorological [[month day] hemisphere]))
  ([seasons hemisphere month day]
   (get-in seasons [[month day] hemisphere])))


(which-season :north :march 5) ;=> :spring
(which-season :south :december 25) ;=> :summer

(which-season seasons-astronomical :north :march 5) ;=> :winter
(which-season seasons-astronomical :south :december 25) ;=> :summer

@sztamas
Copy link

sztamas commented Sep 18, 2020

;; Vector of seasons in order containing the months in each season.
;; Starts with spring in northern hemisphere (autumn in southern hemisphere).
(def seasons [#{:march :april :may}
              #{:june :july :august}
              #{:september :october :november}
              #{:december :january :february}])

;; Can be left out if we don't validate days
(defn max-days-in-month [month]
  (condp contains? month
    #{:february}                         29
    #{:april :june :september :november} 30
    31))

(def season-names {:north [:spring :summer :autumn :winter]
                   :south [:autumn :winter :spring :summer]})

(defn season-index [month]
  (first (keep-indexed #(when (%2 month) %1) seasons)))

(defn which-season
  [hemisphere month day]
  {:pre [(contains? season-names hemisphere)
         (season-index month)
         (<= 1 day (max-days-in-month month))]}
  (let [seasons (hemisphere season-names)]
    (seasons (season-index month))))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment