368 lines
13 KiB
Zig
368 lines
13 KiB
Zig
const std = @import("std");
|
|
const zeit = @import("zeit");
|
|
const types = @import("../weather/types.zig");
|
|
const emoji = @import("emoji.zig");
|
|
const utils = @import("utils.zig");
|
|
const Moon = @import("../Moon.zig");
|
|
const Astronomical = @import("../Astronomical.zig");
|
|
const TimeZoneOffsets = @import("../location/timezone_offsets.zig");
|
|
const Coordinates = @import("../Coordinates.zig");
|
|
|
|
pub fn render(writer: *std.Io.Writer, weather: types.WeatherData, format: []const u8, use_imperial: bool) !void {
|
|
var i: usize = 0;
|
|
while (i < format.len) {
|
|
if (format[i] == '%' and i + 1 < format.len) {
|
|
const code = format[i + 1];
|
|
switch (code) {
|
|
'c' => try writer.writeAll(emoji.getWeatherEmoji(weather.current.weather_code)),
|
|
'C' => try writer.writeAll(weather.current.condition),
|
|
'h' => try writer.print("{d}%", .{weather.current.humidity}),
|
|
't' => {
|
|
const temp = if (use_imperial) weather.current.tempFahrenheit() else weather.current.temp_c;
|
|
const unit = if (use_imperial) "°F" else "°C";
|
|
const sign: u8 = if (temp >= 0) '+' else 0;
|
|
if (sign != 0) {
|
|
try writer.print("{c}{d:.1}{s}", .{ sign, temp, unit });
|
|
} else {
|
|
try writer.print("{d:.1}{s}", .{ temp, unit });
|
|
}
|
|
},
|
|
'f' => {
|
|
const temp = if (use_imperial) weather.current.tempFahrenheit() else weather.current.temp_c;
|
|
const unit = if (use_imperial) "°F" else "°C";
|
|
const sign: u8 = if (temp >= 0) '+' else 0;
|
|
if (sign != 0) {
|
|
try writer.print("{c}{d:.1}{s}", .{ sign, temp, unit });
|
|
} else {
|
|
try writer.print("{d:.1}{s}", .{ temp, unit });
|
|
}
|
|
},
|
|
'w' => {
|
|
const wind = if (use_imperial) weather.current.windMph() else weather.current.wind_kph;
|
|
const unit = if (use_imperial) "mph" else "km/h";
|
|
try writer.print("{d:.0} {s} {s}", .{ wind, unit, utils.degreeToDirection(weather.current.wind_deg) });
|
|
},
|
|
'l' => try writer.writeAll(weather.locationDisplayName()),
|
|
'p' => {
|
|
const precip = if (use_imperial) weather.current.precip_mm * 0.0393701 else weather.current.precip_mm;
|
|
const unit = if (use_imperial) "in" else "mm";
|
|
try writer.print("{d:.1} {s}", .{ precip, unit });
|
|
},
|
|
'P' => {
|
|
const pressure = if (use_imperial) weather.current.pressure_mb * 0.02953 else weather.current.pressure_mb;
|
|
const unit = if (use_imperial) "inHg" else "hPa";
|
|
try writer.print("{d:.2} {s}", .{ pressure, unit });
|
|
},
|
|
'm' => {
|
|
const now = try nowAt(weather.coords);
|
|
const moon = Moon.getPhase(now);
|
|
try writer.writeAll(moon.emoji());
|
|
},
|
|
'M' => {
|
|
const now = try nowAt(weather.coords);
|
|
const moon = Moon.getPhase(now);
|
|
try writer.print("{d}", .{moon.day()});
|
|
},
|
|
'o' => try writer.print("0%", .{}), // Probability of precipitation placeholder
|
|
'D', 'S', 'z', 's', 'd' => {
|
|
// Again...we only need approximate, because we simply need
|
|
// to make sure the day is correct for this. Even a day off
|
|
// should actually be ok. Unix timestamp is always UTC,
|
|
// so we convert to local
|
|
const now = try nowAt(weather.coords);
|
|
const astro = Astronomical.init(
|
|
weather.coords.latitude,
|
|
weather.coords.longitude,
|
|
now,
|
|
);
|
|
const utc_time = switch (code) {
|
|
'D' => astro.dawn,
|
|
'S' => astro.sunrise,
|
|
'z' => astro.zenith,
|
|
's' => astro.sunset,
|
|
'd' => astro.dusk,
|
|
else => unreachable,
|
|
};
|
|
try writer.print("{f}", .{try utc_time.adjustTimeToLocal(weather.coords)});
|
|
},
|
|
'%' => try writer.print("%", .{}),
|
|
'n' => try writer.print("\n", .{}),
|
|
else => {
|
|
try writer.print("%{c}", .{code});
|
|
},
|
|
}
|
|
i += 2;
|
|
} else {
|
|
try writer.writeByte(format[i]);
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn nowAt(coords: Coordinates) !i64 {
|
|
const now = try zeit.instant(.{});
|
|
const offset = TimeZoneOffsets.getTimezoneOffset(coords);
|
|
const new = if (offset >= 0)
|
|
try now.add(.{ .minutes = @abs(offset) })
|
|
else
|
|
try now.subtract(.{ .minutes = @abs(offset) });
|
|
return new.unixTimestamp();
|
|
}
|
|
|
|
const test_weather = types.WeatherData{
|
|
// SAFETY: allocator unused in these tests
|
|
.allocator = undefined,
|
|
.location = "Test",
|
|
.display_name = null,
|
|
.coords = .{ .latitude = 0, .longitude = 0 },
|
|
.current = .{
|
|
.temp_c = 10,
|
|
.feels_like_c = 10,
|
|
.condition = "Clear",
|
|
.weather_code = .clear,
|
|
.humidity = 50,
|
|
.wind_kph = 10,
|
|
.wind_deg = 0,
|
|
.pressure_mb = 1013,
|
|
.precip_mm = 0,
|
|
.visibility_km = 10,
|
|
},
|
|
.forecast = &.{},
|
|
};
|
|
|
|
test "render custom format with location and temp" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
const weather = types.WeatherData{
|
|
.location = "London",
|
|
.coords = .{ .latitude = 0, .longitude = 0 },
|
|
.current = .{
|
|
.temp_c = 7.0,
|
|
.feels_like_c = 7.0,
|
|
.condition = "Overcast",
|
|
.weather_code = .clouds_overcast,
|
|
.humidity = 76,
|
|
.wind_kph = 11.0,
|
|
.wind_deg = 22.5,
|
|
.pressure_mb = 1019.0,
|
|
.precip_mm = 0.0,
|
|
.visibility_km = null,
|
|
},
|
|
.forecast = &[_]types.ForecastDay{},
|
|
.allocator = allocator,
|
|
};
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, weather, "%l: %c %t", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
|
|
try std.testing.expect(std.mem.indexOf(u8, output, "London") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, output, "+7.0°C") != null);
|
|
}
|
|
|
|
test "render custom format with newline" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
const weather = types.WeatherData{
|
|
.location = "Paris",
|
|
.coords = .{ .latitude = 0, .longitude = 0 },
|
|
.current = .{
|
|
.temp_c = 10.0,
|
|
.feels_like_c = 10.0,
|
|
.condition = "Clear",
|
|
.weather_code = .clear,
|
|
.humidity = 65,
|
|
.wind_kph = 8.0,
|
|
.wind_deg = 90.0,
|
|
.pressure_mb = 1020.0,
|
|
.precip_mm = 0.0,
|
|
.visibility_km = null,
|
|
},
|
|
.forecast = &[_]types.ForecastDay{},
|
|
.allocator = allocator,
|
|
};
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, weather, "%l%n%C", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
|
|
try std.testing.expect(std.mem.indexOf(u8, output, "Paris\nClear") != null);
|
|
}
|
|
|
|
test "render custom format with humidity and pressure" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
const weather = types.WeatherData{
|
|
.location = "Berlin",
|
|
.coords = .{ .latitude = 0, .longitude = 0 },
|
|
.current = .{
|
|
.temp_c = 5.0,
|
|
.feels_like_c = 5.0,
|
|
.condition = "Cloudy",
|
|
.weather_code = .clouds_overcast,
|
|
.humidity = 85,
|
|
.wind_kph = 12.0,
|
|
.wind_deg = 270.0,
|
|
.pressure_mb = 1012.0,
|
|
.precip_mm = 0.2,
|
|
.visibility_km = null,
|
|
},
|
|
.forecast = &[_]types.ForecastDay{},
|
|
.allocator = allocator,
|
|
};
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, weather, "Humidity: %h, Pressure: %P", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
|
|
try std.testing.expect(std.mem.indexOf(u8, output, "85%") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, output, "1012") != null);
|
|
}
|
|
|
|
test "render custom format with imperial units" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
const weather = types.WeatherData{
|
|
.location = "NYC",
|
|
.coords = .{ .latitude = 0, .longitude = 0 },
|
|
.current = .{
|
|
.temp_c = 10.0,
|
|
.feels_like_c = 10.0,
|
|
.condition = "Clear",
|
|
.weather_code = .clear,
|
|
.humidity = 60,
|
|
.wind_kph = 16.0,
|
|
.wind_deg = 0.0,
|
|
.pressure_mb = 1013.0,
|
|
.precip_mm = 2.5,
|
|
.visibility_km = null,
|
|
},
|
|
.forecast = &[_]types.ForecastDay{},
|
|
.allocator = allocator,
|
|
};
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, weather, "%t %w %p", true);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
|
|
try std.testing.expect(std.mem.indexOf(u8, output, "+50.0°F") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, output, "mph") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, output, "in") != null);
|
|
}
|
|
|
|
test "render custom format with feels like temp" {
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather, "%f", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("+10.0°C", output);
|
|
}
|
|
|
|
test "render custom format with moon phase" {
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather, "%m", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("🌗", output);
|
|
}
|
|
|
|
test "render custom format with moon day" {
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather, "%M", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("21", output);
|
|
}
|
|
|
|
test "render custom format with astronomical dawn" {
|
|
var test_weather_astro = test_weather;
|
|
test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 };
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather_astro, "%D", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("07:12", output);
|
|
}
|
|
|
|
test "render custom format with astronomical sunrise" {
|
|
var test_weather_astro = test_weather;
|
|
test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 };
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather_astro, "%S", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("07:45", output);
|
|
}
|
|
|
|
test "render custom format with astronomical zenith" {
|
|
var test_weather_astro = test_weather;
|
|
test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 };
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather_astro, "%z", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("12:14", output);
|
|
}
|
|
|
|
test "render custom format with astronomical sunset" {
|
|
var test_weather_astro = test_weather;
|
|
test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 };
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather_astro, "%s", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("16:44", output);
|
|
}
|
|
|
|
test "render custom format with astronomical dusk" {
|
|
var test_weather_astro = test_weather;
|
|
test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 };
|
|
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather_astro, "%d", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("17:17", output);
|
|
}
|
|
|
|
test "render custom format with percent sign" {
|
|
var output_buf: [1024]u8 = undefined;
|
|
var writer = std.Io.Writer.fixed(&output_buf);
|
|
|
|
try render(&writer, test_weather, "%%", false);
|
|
|
|
const output = output_buf[0..writer.end];
|
|
try std.testing.expectEqualStrings("%", output);
|
|
}
|