Skip to content

Instantly share code, notes, and snippets.

@ericnormand

ericnormand/00 Seasons.md

Last active Sep 18, 2020
Embed
What would you like to do?
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.

@steffan-westcott

This comment has been minimized.

Copy link

@steffan-westcott steffan-westcott commented Sep 14, 2020

(def seasons
  [
    [[:march :april :may]            :spring :autumn]
    [[:june :july :august]           :summer :winter]
    [[:september :october :november] :autumn :spring]
    [[:december :january :february]  :winter :summer] 
  ])

(def season-for-month-hemisphere
  (into {}
    (for [[months north south] seasons
          :let [season-for-hemi {:north north :south south}]
          month months]
      [month season-for-hemi])))

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

I aimed here to find a reasonable way to encode the table in the spec as a nested map.

@RedPenguin101

This comment has been minimized.

Copy link

@RedPenguin101 RedPenguin101 commented Sep 14, 2020

(def seasons
  {:january {:north :winter :south :summer}
   :february {:north :winter :south :summer}
   :march {:north :spring :south :autumn}
   :april {:north :spring :south :autumn}
   :may {:north :spring :south :autumn}
   :june {:north :summer :south :winter}
   :july {:north :summer :south :winter}
   :august {:north :summer :south :winter}
   :september {:north :autumn :south :spring}
   :october {:north :autumn :south :spring}
   :november {:north :autumn :south :spring}
   :december {:north :winter :south :summer}})

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

KISS

@mchampine

This comment has been minimized.

Copy link

@mchampine mchampine commented Sep 14, 2020

(defn which-season [h m _]
  (let [seasons [:winter :spring :summer :autumn]
        months  [:december :january :february :march :april :may
                 :june :july :august :september :october :november]
        season (m (zipmap months (mapcat #(repeat 3 %) seasons)))
        nsflip (zipmap seasons (drop 2 (cycle seasons)))]
    (if (= h :north) season (nsflip season))))

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

(ns myns (:require [clojure.string :as s] [java-time :as jt]))
(map (comp keyword
           s/lower-case
           (partial jt/format "MMMM")
           (partial jt/zoned-date-time 1))
     (cons 12 (range 1 12)))

;;=> (:december :january :february :march :april :may :june :july :august :september :october :november)

Doesn't seem worth it. :)

@emeraldimp

This comment has been minimized.

Copy link

@emeraldimp emeraldimp commented Sep 14, 2020

Are we limited to Clojure? I stayed on the simple path with F#. I thought about trying to condense it, but straight up listing it works well enough. If we had to consider the day (e.g. each season usually starts on the 20th or so), it would probably be clearer to break it up a bit.

    module SeasonChallenge =
    
      type Hemisphere = North | South

      // This might exist elsewhere, but it was easy enough to include
      type Month = January | February | March | April | May | June | July | August | September | October | November | December
    
      type Season = Spring | Summer | Autumn | Winter
    
      // day isn't used in this challenge, but we need to conform to the expected api
      let whichSeason hemisphere month day =
          match (hemisphere, month) with
              | (North, March) -> Spring
              | (North, April) -> Spring
              | (North, May) -> Spring
              | (North, June) -> Summer
              | (North, July) -> Summer
              | (North, August) -> Summer
              | (North, September) -> Autumn
              | (North, October) -> Autumn
              | (North, November) -> Autumn
              | (North, December) -> Winter
              | (North, January) -> Winter
              | (North, February) -> Winter
              | (South, March) -> Autumn
              | (South, April) -> Autumn
              | (South, May) -> Autumn
              | (South, June) -> Winter
              | (South, July) -> Winter
              | (South, August) -> Winter
              | (South, September) -> Spring
              | (South, October) -> Spring
              | (South, November) -> Spring
              | (South, December) -> Summer
              | (South, January) -> Summer
              | (South, February) -> Summer
@cloojure

This comment has been minimized.

Copy link

@cloojure cloojure commented Sep 14, 2020

(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

This comment has been minimized.

Copy link

@KingCode 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

This comment has been minimized.

Copy link

@daveschoutens 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

This comment has been minimized.

Copy link

@mchampine 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

This comment has been minimized.

Copy link

@claj 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

This comment has been minimized.

Copy link

@dfuenzalida 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

This comment has been minimized.

Copy link

@emeraldimp emeraldimp commented Sep 16, 2020

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

This comment has been minimized.

Copy link

@msladecek 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

This comment has been minimized.

Copy link

@sztamas 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
You can’t perform that action at this time.