Give it a US ZIP code and it draws the next 72 hours off the National Weather Service — temperature, what it feels like, conditions, precipitation, humidity and wind. The rows are banded light and dark for day and night, with a bold marker at each sunrise and sunset.
cd mills/weather-glass
./build.sh run 63011 # the network is needed for this one
./build.sh test # and never for this one
63011 Ballwin, Missouri
Mon Aug 3, 2026 - NWS (api.weather.gov)
Date Hr Temp (FL) Conditions Prcp Cld Hmd Dir Spd
Mo 3 15 85 (88) Mstly Sunny 0% 25% 55% NE 1
Mo 3 16 85 (87) Mstly Sunny 0% 25% 53% NE 2
Mo 3 17 84 (86) Sunny 0% 5% 55% E 2
v Sunset 8:12 PM -- Lo 66, Mstly Clear, winds to 3 mph
Mo 3 18 83 (85) Mstly Clear 0% 25% 57% E 2
A weather-glass is what a barometer used to be called: the instrument in the hall you tapped to read the coming weather.
| # | Host | What it gives |
|---|---|---|
| 1 | api.zippopotam.us | The ZIP becomes a place name and coordinates. |
| 2 | api.weather.gov/points | The coordinates become two forecast URLs. |
| 3 | the daily forecast | The highs, lows and wind for the marker rows. |
| 4 | the hourly forecast | The rows themselves. |
Every hop is status-gated: the reply's status code — the number a web server sends back to say how the request went — is checked before anything else happens. Shoddy has no catchable errors, so a clean abort naming the hop is the error handling. An unknown ZIP stops at hop one and says so. A non-200 status (200 is the code for success) anywhere names the host, the path and the status.
Both hosts get a User-Agent — the header that tells a
server who is calling. The weather service answers 403
(forbidden) without one, and zippopotam gets one out of courtesy.
Coordinates are trimmed to four decimals before hop two. The API — the
address a service answers programs at, as opposed to a page people read —
has historically redirected long ones, answering "ask this other address
instead", and this client follows no redirects.
The shell does four fetches and a Print. Everything else
is pure — computed from its inputs, touching nothing outside: parsing, the
calendar, the sun, the feels-like formulas, the abbreviation table, the
banding and all 58 columns of layout. It lives in
weather-glass-core.shoddy and weather-glass-sun.shoddy:
Def Render(p As Place, hours As List Of Hour, days As List Of DayInfo) As List Of String
Records in, every line of the report out. So test.shoddy
feeds it the captured API responses in files/ and compares
the result against files/expected.out line by line, with
no network and no --allow-net. Escape
sequences — the character codes that set a terminal's colours and styles —
are bytes like any others. The golden file, the saved known-good output,
simply contains them.
It is the same split mungo-caverns draws by making a turn a function, and emley-moor draws by making a request one. Put the I/O — the input and output — at the edge, and the middle becomes gradeable.
There is no fifth hop for sunrise and sunset.
weather-glass-sun.shoddy carries the solar-position algorithm
published by NOAA, the US weather and oceans agency — fractional year,
equation of time, solar declination, hour angle at a zenith of 90.833° —
in about forty lines of arithmetic. It uses
math's Rad and
Deg and the trig builtins. It is graded against published
values to within two minutes. The reference rendering this mill mirrors is
itself one of those values: it puts sunset on 2 May 2026 at 7:56 PM, and
the algorithm computes 7:54.
Above the Arctic circle in June the sun neither rises nor sets. The
cosine then falls outside the range of inputs Acos accepts.
So that is checked before the call, and such a day reports -1
rather than aborting.
The weekday comes from Sakamoto's congruence — a short piece of arithmetic that turns any date into its day of the week. Five lines and a twelve-entry table, with no date type anywhere in the language or in the mill.
Cld column is inferred from the
words in the forecast: "Mostly Sunny" becomes 25%, "Cloudy" 95%,
anything raining 75%. A condition the table does not recognise renders as
a blank cell rather than a guess. Every other number in
the table came from the API.The hourly feed gives temperature, humidity and wind. Those are exactly the inputs of the two standard NOAA formulas: wind chill below 50 °F with a wind over 3 mph, and the Rothfusz heat-index regression — a formula fitted to measured data — above 80 °F with its two humidity adjustments. Both are transcribed rather than improvised. Both are checked against the published tables: 35 °F at 10 mph reads 27, and 90 °F at 70% humidity reads within a degree of the chart's 105. Between the two ranges, what it is is what it feels like.
The reference rendering puts one space between some fields and two between others. That is reproduced exactly rather than tidied into a uniform grid. A report that is nearly the reference is a report somebody has to check by eye forever. A test asserts the header is byte-identical and that every row is the same width.
The captured responses — the test's fixture, its saved input data — are
the live ones, with a single edit: one hour's
probabilityOfPrecipitation is set to null, the
value that means "no data here". That day's capture happened to contain
none, but the API does return null there. A mill that aborts at three in
the morning because precipitation was null has not used the total
accessors json provides — the readers
that always return something instead of failing. The edit is the
test.
| Machine | Why | |
|---|---|---|
| https | All four hops, over TLS — the encryption that puts the s in https — with the status and headers read back by its pure half. | |
| json | Every response.
Object keys with spaces in them, coordinates that arrive as strings, and a
null that has to read as zero. | |
| math | Rad and
Deg for the solar algorithm. | |
| vt100 | The banding: day rows plain, night rows dim, markers bold, header reversed. | |
| file | The offline test reads the fixtures and the golden render. | |
| seq | Taken cuts the
feed to 72 hours; the render accumulates with Prepend. | |
| str | PadLeft,
PadRight, PadZero and ToFixed lay out the
columns. |