Brad Lucas

Programming, Clojure and other interests
June 22, 2017

Building A Weather App In Clojure

Overview

I stumbled upon this service called Dark Sky the other day which supports an API to return weather reports. I thought it would be interesting to create a small command line application which would query this API and print out a short summary weather report. Yesterday, I took up the challenge to build such an application in Clojure.

The results are up on GitHub in the following repo.

https://github.com/bradlucas/weather

For those who are interested I'll go through a process to build the application.

Requirements

First, investigate the Dark Sky API. The documention is here:

The main function is here:

It is fairly straightforward. You register and get a key which you pass to a web service url which returns data in JSON format. In addition to the key you need to pass along latitude and longitude coordinates for the location where you'd like the weather reports for.

Register for a key here:

Location

With the requirement that lat and lng values are needed, I considered if it was possible to convert a zipcode to these values. I'm figuring that a zipcode would be a better input parameter for a user.

Searching around it looks like someone has put the needed mapping together in a Gist.

This looked like it would work. Read the raw file, parse the csv and return the lat and lng values for an associated `zipcode'.

Data

As a first step, find the lat and lng values for your given zipcode and then build the Dark Sky API url manually with your new key. The format of which is:

https://api.darksky.net/forecast/KEY/LAT,LNG

You can put that in a browser and make sure it works. Investigate the results. You'll see there is a lot of data.

Example

For my purposes I identified the following as useful:

  • currently/summary
  • hourly/summary
  • daily/summary

Coding Strategy

  • Accept a zipcode from the command line
  • Function to convert a zipcode to lat and lng values
  • Pull API key from an environment variable
  • Build API url and get weather data
  • Parse data and return summary for output

zipcode.clj

To make requests from other sites I'm using the clj-http library. It's get function returns a map which you'll want to get the value for :body. This data is in csv format so you need to parse it. Here I'm using clojure.data.csv to parse the data. When you try it you'll see that it returns a sequence of vectors which include each row's data elements. Since the first is the zipcode filter on that and return the second and third elements as they are the lat and lng valuues.

(ns weather.zipcode
  (:require [clj-http.client :as client]
            [clojure.data.csv :as csv]))


(def zip-data-url 
  "Found data at https://gist.github.com/erichurst/7882666"
  "https://gist.githubusercontent.com/erichurst/7882666/raw/5bdc46db47d9515269ab12ed6fb2850377fd869e/US%20Zip%20Codes%20from%202013%20Government%20Data")

(defn get-zipcode-location
  "Return lat, lng for zipcode"
  [zipcode]
  (let [m (csv/read-csv (:body (client/get zip-data-url)))
        [_ lat lng] (first (filter #(= zipcode (first %)) m))]
    [lat lng]))

data.clj

The data namespace holds the main getter function. Here we need to get our key. I'm using environ to pull the value from the environment. This means you need to setup an environment variable as follows before running the application.

export DARKSKY_KEY=KEY-VALUE

Next, call into the zipcode namespace to get the lat and lng values with get-zipccode-location function. With that the API url can be built.

Since, the API returns JSON you'll want to convert that to something easy use in Clojure. The Cheshire library is good for this and the function parse-string converts the JSON data to a Clojure map.

Once in a map it is easy to get-in the values.

(ns weather.data
  (:require [clj-http.client :as client]
            [weather.zipcode :as zip]
            [cheshire.core :as cheshire :refer [parse-string]]
            [environ.core :refer [env]]))


(def darksky-key 
  "Configure your DarkSky key as an environment variable"
  (env :darksky-key))

(defn get-weather-data 
  "Contact darksky for the data" 
  [lat lng]
  (let [url (format "https://api.darksky.net/forecast/%s/%s,%s" darksky-key lat lng)]
    (client/get url)))

(defn weather-report [zipcode]
  "Return a report for the weather data"
  (let [[lat lng] (zip/get-zipcode-location zipcode)
        m (cheshire/parse-string (:body (get-weather-data lat lng)))]
    (let [current (get-in m ["currently" "summary"])
          soon (get-in m ["hourly" "summary"])
          later (get-in m ["daily" "summary"])]
      [current soon later])))

core.clj

Lastly, the -main function is where to accept the command line parameter zipcode, call into data/weather-report and then display the results.

(ns weather.core
 (:require [weather.data :as data])
 (:gen-class))


(defn -main 
  "Accept a zipcode on the command line and produce a weather report ensure single argument" 
  [& args]
  (if args
    (let [zipcode (first args)]
      (let [[current soon later] (data/weather-report zipcode)]
        (println (format "Zipcode  : %s" zipcode))
        (println (format "Current  : %s" current))
        (println (format "Soon     : %s" soon))
        (println (format "Later    : %s" later))))
    (println "Usage: weather ZIPCODE")))

Summary

Here is an example run for New York city.

$ lein run 10001
Zipcode  : 10001
Current  : Partly Cloudy
Soon     : Mostly cloudy starting this afternoon.
Later    : Rain tomorrow and Saturday, with temperatures falling to 78°F on Tuesday.

Some other large city zipcodes for trial:

|-------------+----------|
| City        | Zip Code |
|-------------+----------|
| New York    |    10002 |
| Boston      |    02114 |
| Los Angeles |    90011 |
| Chicago     |    60629 |
| Austin      |    73301 |
|-------------+----------|

For subsequent versions of this application, I foresee a few things:

  • The zipcode data could be stored locally. It probably doesn't change much and saving a network call would speed things up. I'd consider storing it already parsed as well so the csv parsing isn't required. Also, there should be some error handling to catch unknown or invalid zipcodes.
  • It would be nice to enter a location instead of a zipcode. Maybe there are mappings from location names to zip codes available.
  • This would make a nice small web app. Enter a zip and see results. Maybe a ClojureScript app.
  • Lastly, there is certain a ton more data being returned by the API. Maybe this could be used in a clever way.
Tags: clojure