The Mills · Demos

weather-glass

A 72-hour forecast in VT100 — mills/weather-glass

A barometer dial beside a column of forecast rows

Summary

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.

Four hops

#HostWhat it gives
1api.zippopotam.usThe ZIP becomes a place name and coordinates.
2api.weather.gov/pointsThe coordinates become two forecast URLs.
3the daily forecastThe highs, lows and wind for the marker rows.
4the hourly forecastThe 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 seam

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.

Under the Hood

The sun is computed, not fetched

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.

The cloud column is an estimate

Say it plainly. The hourly feed carries no sky cover. That number lives only in the raw gridpoint endpoint — a separate address the service answers — and fetching it would be a fifth hop of some 212 KB, timed in UTC intervals (UTC is the world's shared reference clock). So the 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.

What it feels like

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 layout is 58 columns with uneven gaps

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 fixture has one deliberate lie in it

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.

The Machines It Uses

MachineWhy
httpsAll four hops, over TLS — the encryption that puts the s in https — with the status and headers read back by its pure half.
jsonEvery response. Object keys with spaces in them, coordinates that arrive as strings, and a null that has to read as zero.
mathRad and Deg for the solar algorithm.
vt100The banding: day rows plain, night rows dim, markers bold, header reversed.
fileThe offline test reads the fixtures and the golden render.
seqTaken cuts the feed to 72 hours; the render accumulates with Prepend.
strPadLeft, PadRight, PadZero and ToFixed lay out the columns.