Fix weather, yes/no, and news integrations
This commit is contained in:
@@ -595,9 +595,19 @@ public sealed class JiboInteractionService(
|
||||
{
|
||||
var referenceLocalTime = TryResolveReferenceLocalTime(turn);
|
||||
var normalizedTranscript = NormalizeCommandPhrase(transcript);
|
||||
var locationQuery = TryResolveWeatherLocationQuery(transcript);
|
||||
var weatherDate = ResolveWeatherDateEntity(turn, transcript, normalizedTranscript, referenceLocalTime);
|
||||
var isRangeForecastRequest = IsRangeForecastRequest(normalizedTranscript);
|
||||
if (ShouldDefaultForecastToTomorrow(normalizedTranscript, weatherDate, isRangeForecastRequest))
|
||||
var isOpenEndedForecastRequest = IsOpenEndedForecastRequest(
|
||||
normalizedTranscript,
|
||||
weatherDate,
|
||||
isRangeForecastRequest,
|
||||
locationQuery);
|
||||
if (ShouldDefaultForecastToTomorrow(
|
||||
normalizedTranscript,
|
||||
weatherDate,
|
||||
isRangeForecastRequest,
|
||||
isOpenEndedForecastRequest))
|
||||
{
|
||||
weatherDate = new WeatherDateEntity("tomorrow", 1, "Tomorrow");
|
||||
}
|
||||
@@ -609,7 +619,6 @@ public sealed class JiboInteractionService(
|
||||
"I can check weather once my weather service is connected.");
|
||||
}
|
||||
|
||||
var locationQuery = TryResolveWeatherLocationQuery(transcript);
|
||||
var weatherCoordinates = string.IsNullOrWhiteSpace(locationQuery)
|
||||
? TryResolveWeatherCoordinates(turn)
|
||||
: null;
|
||||
@@ -617,7 +626,7 @@ public sealed class JiboInteractionService(
|
||||
var isNextWeekForecast = IsNextWeekForecastRequest(normalizedTranscript, isRangeForecastRequest);
|
||||
var isThisWeekForecast = IsThisWeekForecastRequest(normalizedTranscript, isRangeForecastRequest);
|
||||
|
||||
if (isNextWeekForecast || isThisWeekForecast)
|
||||
if (isNextWeekForecast || isThisWeekForecast || isOpenEndedForecastRequest)
|
||||
{
|
||||
var rangeStartOffset = 1;
|
||||
var rangeEndOffset = isThisWeekForecast
|
||||
@@ -920,6 +929,32 @@ public sealed class JiboInteractionService(
|
||||
!normalizedTranscript.Contains("weekend", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsOpenEndedForecastRequest(
|
||||
string normalizedTranscript,
|
||||
WeatherDateEntity weatherDate,
|
||||
bool isRangeForecastRequest,
|
||||
string? locationQuery)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(normalizedTranscript) ||
|
||||
!string.IsNullOrWhiteSpace(locationQuery) ||
|
||||
isRangeForecastRequest ||
|
||||
weatherDate.ForecastDayOffset > 0 ||
|
||||
!normalizedTranscript.Contains("forecast", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !MatchesAny(
|
||||
normalizedTranscript,
|
||||
"today",
|
||||
"today s",
|
||||
"today's",
|
||||
"tonight",
|
||||
"right now",
|
||||
"current weather",
|
||||
"currently");
|
||||
}
|
||||
|
||||
private static int ResolveThisWeekForecastEndOffset(DateTimeOffset? referenceLocalTime)
|
||||
{
|
||||
var resolvedReference = referenceLocalTime ?? DateTimeOffset.UtcNow;
|
||||
@@ -931,9 +966,11 @@ public sealed class JiboInteractionService(
|
||||
private static bool ShouldDefaultForecastToTomorrow(
|
||||
string normalizedTranscript,
|
||||
WeatherDateEntity weatherDate,
|
||||
bool isRangeForecastRequest)
|
||||
bool isRangeForecastRequest,
|
||||
bool isOpenEndedForecastRequest)
|
||||
{
|
||||
if (weatherDate.ForecastDayOffset > 0 ||
|
||||
isOpenEndedForecastRequest ||
|
||||
isRangeForecastRequest ||
|
||||
string.IsNullOrWhiteSpace(normalizedTranscript) ||
|
||||
!normalizedTranscript.Contains("forecast", StringComparison.Ordinal))
|
||||
@@ -1643,6 +1680,7 @@ public sealed class JiboInteractionService(
|
||||
};
|
||||
}
|
||||
|
||||
var yesNoRule = ReadPrimaryYesNoRule(clientRules, listenRules);
|
||||
if (!string.IsNullOrWhiteSpace(pendingProactivityOffer) &&
|
||||
string.Equals(pendingProactivityOffer, "pizza_fact", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -1659,14 +1697,15 @@ public sealed class JiboInteractionService(
|
||||
|
||||
if (isYesNoTurn)
|
||||
{
|
||||
if (IsAffirmativeReply(loweredTranscript))
|
||||
var yesNoReply = TryClassifyYesNoReply(NormalizeCommandPhrase(loweredTranscript));
|
||||
if (yesNoReply == YesNoReply.Affirmative)
|
||||
{
|
||||
return "yes";
|
||||
return ResolveAffirmativeYesNoIntent(yesNoRule);
|
||||
}
|
||||
|
||||
if (IsNegativeReply(loweredTranscript))
|
||||
if (yesNoReply == YesNoReply.Negative)
|
||||
{
|
||||
return "no";
|
||||
return ResolveNegativeYesNoIntent(yesNoRule);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2369,15 +2408,55 @@ public sealed class JiboInteractionService(
|
||||
return ReadRules(turn, "listenRules")
|
||||
.Concat(ReadRules(turn, "clientRules"))
|
||||
.Concat(ReadRules(turn, "listenAsrHints"))
|
||||
.Any(static rule =>
|
||||
string.Equals(rule, "$YESNO", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "clock/alarm_timer_change", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "clock/alarm_timer_none_set", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "shared/yes_no", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "surprises-date/offer_date_fact", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase));
|
||||
.Any(IsYesNoRule);
|
||||
}
|
||||
|
||||
private static string? ReadPrimaryYesNoRule(
|
||||
IReadOnlyList<string> clientRules,
|
||||
IReadOnlyList<string> listenRules)
|
||||
{
|
||||
return listenRules
|
||||
.Concat(clientRules)
|
||||
.FirstOrDefault(IsConstrainedYesNoRule);
|
||||
}
|
||||
|
||||
private static bool IsYesNoRule(string rule)
|
||||
{
|
||||
return string.Equals(rule, "$YESNO", StringComparison.OrdinalIgnoreCase) ||
|
||||
IsConstrainedYesNoRule(rule);
|
||||
}
|
||||
|
||||
private static bool IsConstrainedYesNoRule(string rule)
|
||||
{
|
||||
return string.Equals(rule, "clock/alarm_timer_change", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "clock/alarm_timer_none_set", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "create/is_it_a_keeper", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "shared/yes_no", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "surprises-date/offer_date_fact", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "word-of-the-day/surprise", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ResolveAffirmativeYesNoIntent(string? yesNoRule)
|
||||
{
|
||||
if (string.Equals(yesNoRule, "word-of-the-day/surprise", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "word_of_the_day";
|
||||
}
|
||||
|
||||
if (string.Equals(yesNoRule, "surprises-date/offer_date_fact", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "surprise";
|
||||
}
|
||||
|
||||
return "yes";
|
||||
}
|
||||
|
||||
private static string ResolveNegativeYesNoIntent(string? yesNoRule)
|
||||
{
|
||||
_ = yesNoRule;
|
||||
return "no";
|
||||
}
|
||||
|
||||
private static string? FindClosestHint(string normalizedTranscript, IReadOnlyList<string> hints)
|
||||
@@ -2642,7 +2721,7 @@ public sealed class JiboInteractionService(
|
||||
}
|
||||
}
|
||||
|
||||
return YesNoReply.None;
|
||||
return TryClassifyTrailingYesNoReply(tokens);
|
||||
}
|
||||
|
||||
private static bool TryTrimLeadingAcknowledgement(string normalizedTranscript, out string trimmedTranscript)
|
||||
@@ -2666,6 +2745,70 @@ public sealed class JiboInteractionService(
|
||||
return false;
|
||||
}
|
||||
|
||||
private static YesNoReply TryClassifyTrailingYesNoReply(IReadOnlyList<string> tokens)
|
||||
{
|
||||
var selectedReply = YesNoReply.None;
|
||||
var selectedIndex = -1;
|
||||
|
||||
void Consider(YesNoReply candidateReply, int candidateIndex)
|
||||
{
|
||||
if (candidateIndex < 0 || candidateIndex < selectedIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selectedReply = candidateReply;
|
||||
selectedIndex = candidateIndex;
|
||||
}
|
||||
|
||||
for (var index = 0; index < tokens.Count; index += 1)
|
||||
{
|
||||
var token = tokens[index];
|
||||
if (YesNoNegativeLeadTokens.Contains(token))
|
||||
{
|
||||
Consider(YesNoReply.Negative, index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (YesNoAffirmativeLeadTokens.Contains(token))
|
||||
{
|
||||
Consider(YesNoReply.Affirmative, index);
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index + 1 < tokens.Count; index += 1)
|
||||
{
|
||||
var phrase = $"{tokens[index]} {tokens[index + 1]}";
|
||||
if (YesNoNegativeLeadPhrases.Contains(phrase))
|
||||
{
|
||||
Consider(YesNoReply.Negative, index + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (YesNoAffirmativeLeadPhrases.Contains(phrase))
|
||||
{
|
||||
Consider(YesNoReply.Affirmative, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index + 2 < tokens.Count; index += 1)
|
||||
{
|
||||
var phrase = $"{tokens[index]} {tokens[index + 1]} {tokens[index + 2]}";
|
||||
if (YesNoNegativeLeadPhrases.Contains(phrase))
|
||||
{
|
||||
Consider(YesNoReply.Negative, index + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (YesNoAffirmativeLeadPhrases.Contains(phrase))
|
||||
{
|
||||
Consider(YesNoReply.Affirmative, index + 2);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedReply;
|
||||
}
|
||||
|
||||
private static bool IsTimeRequest(string loweredTranscript)
|
||||
{
|
||||
var normalized = NormalizeCommandPhrase(loweredTranscript);
|
||||
@@ -4568,7 +4711,9 @@ public sealed class JiboInteractionService(
|
||||
"well",
|
||||
"so",
|
||||
"actually",
|
||||
"honestly"
|
||||
"honestly",
|
||||
"thanks",
|
||||
"thank you"
|
||||
];
|
||||
|
||||
private static readonly HashSet<string> YesNoAffirmativeLeadTokens = new(StringComparer.Ordinal)
|
||||
|
||||
@@ -1217,7 +1217,8 @@ public sealed partial class WebSocketTurnFinalizationService(
|
||||
string.Equals(rule, "shared/yes_no", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "settings/download_now_later", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "surprises-date/offer_date_fact", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase);
|
||||
string.Equals(rule, "surprises-ota/want_to_download_now", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(rule, "word-of-the-day/surprise", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsYesNoReplyTranscript(string normalizedTranscript)
|
||||
@@ -1287,7 +1288,7 @@ public sealed partial class WebSocketTurnFinalizationService(
|
||||
}
|
||||
}
|
||||
|
||||
return YesNoReply.None;
|
||||
return TryClassifyTrailingYesNoReply(tokens);
|
||||
}
|
||||
|
||||
private static bool TryTrimLeadingAcknowledgement(string normalizedTranscript, out string trimmedTranscript)
|
||||
@@ -1311,6 +1312,70 @@ public sealed partial class WebSocketTurnFinalizationService(
|
||||
return false;
|
||||
}
|
||||
|
||||
private static YesNoReply TryClassifyTrailingYesNoReply(IReadOnlyList<string> tokens)
|
||||
{
|
||||
var selectedReply = YesNoReply.None;
|
||||
var selectedIndex = -1;
|
||||
|
||||
void Consider(YesNoReply candidateReply, int candidateIndex)
|
||||
{
|
||||
if (candidateIndex < 0 || candidateIndex < selectedIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selectedReply = candidateReply;
|
||||
selectedIndex = candidateIndex;
|
||||
}
|
||||
|
||||
for (var index = 0; index < tokens.Count; index += 1)
|
||||
{
|
||||
var token = tokens[index];
|
||||
if (YesNoNegativeLeadTokens.Contains(token))
|
||||
{
|
||||
Consider(YesNoReply.Negative, index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (YesNoAffirmativeLeadTokens.Contains(token))
|
||||
{
|
||||
Consider(YesNoReply.Affirmative, index);
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index + 1 < tokens.Count; index += 1)
|
||||
{
|
||||
var phrase = $"{tokens[index]} {tokens[index + 1]}";
|
||||
if (YesNoNegativeLeadPhrases.Contains(phrase))
|
||||
{
|
||||
Consider(YesNoReply.Negative, index + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (YesNoAffirmativeLeadPhrases.Contains(phrase))
|
||||
{
|
||||
Consider(YesNoReply.Affirmative, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index + 2 < tokens.Count; index += 1)
|
||||
{
|
||||
var phrase = $"{tokens[index]} {tokens[index + 1]} {tokens[index + 2]}";
|
||||
if (YesNoNegativeLeadPhrases.Contains(phrase))
|
||||
{
|
||||
Consider(YesNoReply.Negative, index + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (YesNoAffirmativeLeadPhrases.Contains(phrase))
|
||||
{
|
||||
Consider(YesNoReply.Affirmative, index + 2);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedReply;
|
||||
}
|
||||
|
||||
private async Task ApplyContextUpdatesAsync(
|
||||
CloudSession session,
|
||||
IDictionary<string, object?> contextUpdates,
|
||||
@@ -1910,7 +1975,9 @@ public sealed partial class WebSocketTurnFinalizationService(
|
||||
"well",
|
||||
"so",
|
||||
"actually",
|
||||
"honestly"
|
||||
"honestly",
|
||||
"thanks",
|
||||
"thank you"
|
||||
];
|
||||
|
||||
private static readonly HashSet<string> YesNoAffirmativeLeadTokens = new(StringComparer.Ordinal)
|
||||
|
||||
@@ -79,7 +79,7 @@ public sealed class NewsApiBriefingProvider(
|
||||
foreach (var category in categories)
|
||||
{
|
||||
var uri = BuildTopHeadlinesUri(category, requestedHeadlineCount);
|
||||
using var response = await httpClient.GetAsync(uri, cancellationToken);
|
||||
using var response = await SendGetAsync(uri, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseBody = await TryReadResponseBodySnippetAsync(response, cancellationToken);
|
||||
@@ -172,7 +172,7 @@ public sealed class NewsApiBriefingProvider(
|
||||
string.Join(",", categories));
|
||||
|
||||
var broadUri = BuildTopHeadlinesUri(category: null, requestedHeadlineCount);
|
||||
using var broadResponse = await httpClient.GetAsync(broadUri, cancellationToken);
|
||||
using var broadResponse = await SendGetAsync(broadUri, cancellationToken);
|
||||
if (broadResponse.IsSuccessStatusCode)
|
||||
{
|
||||
using var broadStream = await broadResponse.Content.ReadAsStreamAsync(cancellationToken);
|
||||
@@ -239,7 +239,7 @@ public sealed class NewsApiBriefingProvider(
|
||||
options.FallbackQuery);
|
||||
|
||||
var everythingUri = BuildEverythingUri(requestedHeadlineCount);
|
||||
using var everythingResponse = await httpClient.GetAsync(everythingUri, cancellationToken);
|
||||
using var everythingResponse = await SendGetAsync(everythingUri, cancellationToken);
|
||||
if (everythingResponse.IsSuccessStatusCode)
|
||||
{
|
||||
using var everythingStream = await everythingResponse.Content.ReadAsStreamAsync(cancellationToken);
|
||||
@@ -345,6 +345,20 @@ public sealed class NewsApiBriefingProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendGetAsync(Uri uri, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
request.Headers.TryAddWithoutValidation("User-Agent", ResolveUserAgent());
|
||||
return await httpClient.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
private string ResolveUserAgent()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(options.UserAgent)
|
||||
? DefaultUserAgent
|
||||
: options.UserAgent.Trim();
|
||||
}
|
||||
|
||||
private IEnumerable<string> ResolveCategories(IReadOnlyList<string> preferredCategories)
|
||||
{
|
||||
var requested = preferredCategories
|
||||
@@ -557,6 +571,7 @@ public sealed class NewsApiBriefingProvider(
|
||||
|
||||
private const int MaxHeadlines = 5;
|
||||
private const int MaxCategories = 2;
|
||||
private const string DefaultUserAgent = "OpenJiboCloud/1.0";
|
||||
|
||||
private sealed record ApiError(string? Code, string? Message);
|
||||
private sealed record CacheEntry<T>(T Value, DateTimeOffset ExpiresUtc);
|
||||
|
||||
@@ -6,6 +6,8 @@ public sealed class NewsApiOptions
|
||||
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
public string UserAgent { get; set; } = "OpenJiboCloud/1.0";
|
||||
|
||||
public string Country { get; set; } = "us";
|
||||
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
@@ -173,6 +173,26 @@ public sealed class OpenWeatherReportProvider(
|
||||
var temperature = TryReadInt(main, "temp");
|
||||
var high = TryReadInt(main, "temp_max");
|
||||
var low = TryReadInt(main, "temp_min");
|
||||
if (ShouldEnrichCurrentDayHighLow(temperature, high, low))
|
||||
{
|
||||
try
|
||||
{
|
||||
var enrichedBand = await TryResolveCurrentDayHighLowFromForecastAsync(
|
||||
location,
|
||||
useCelsius,
|
||||
cancellationToken);
|
||||
if (enrichedBand is not null)
|
||||
{
|
||||
high = enrichedBand.Value.High ?? high;
|
||||
low = enrichedBand.Value.Low ?? low;
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
logger.LogDebug(exception, "OpenWeather forecast enrichment failed for current-day Hi/Lo.");
|
||||
}
|
||||
}
|
||||
|
||||
if (temperature is null && high is null && low is null)
|
||||
{
|
||||
return null;
|
||||
@@ -189,6 +209,70 @@ public sealed class OpenWeatherReportProvider(
|
||||
useCelsius);
|
||||
}
|
||||
|
||||
private async Task<(int? High, int? Low)?> TryResolveCurrentDayHighLowFromForecastAsync(
|
||||
LocationPoint location,
|
||||
bool useCelsius,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var forecastUri = BuildRequestUri(
|
||||
"/data/2.5/forecast",
|
||||
("lat", location.Latitude.ToString(CultureInfo.InvariantCulture)),
|
||||
("lon", location.Longitude.ToString(CultureInfo.InvariantCulture)),
|
||||
("units", useCelsius ? "metric" : "imperial"),
|
||||
("appid", options.ApiKey!));
|
||||
using var response = await httpClient.GetAsync(forecastUri, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("list", out var list) || list.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var offset = TryReadForecastOffset(root);
|
||||
var targetDate = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(offset).DateTime);
|
||||
var highs = new List<int>();
|
||||
var lows = new List<int>();
|
||||
foreach (var item in list.EnumerateArray())
|
||||
{
|
||||
if (!TryReadLong(item, "dt", out var unixSeconds))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var localTimestamp = DateTimeOffset.FromUnixTimeSeconds(unixSeconds).ToOffset(offset);
|
||||
if (DateOnly.FromDateTime(localTimestamp.DateTime) != targetDate ||
|
||||
!item.TryGetProperty("main", out var main))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var high = TryReadInt(main, "temp_max");
|
||||
if (high is not null)
|
||||
{
|
||||
highs.Add(high.Value);
|
||||
}
|
||||
|
||||
var low = TryReadInt(main, "temp_min");
|
||||
if (low is not null)
|
||||
{
|
||||
lows.Add(low.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (highs.Count == 0 && lows.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (highs.Count == 0 ? null : highs.Max(), lows.Count == 0 ? null : lows.Min());
|
||||
}
|
||||
|
||||
private async Task<WeatherReportSnapshot?> GetForecastForDayOffsetAsync(
|
||||
LocationPoint location,
|
||||
bool useCelsius,
|
||||
@@ -408,6 +492,21 @@ public sealed class OpenWeatherReportProvider(
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ShouldEnrichCurrentDayHighLow(int? temperature, int? high, int? low)
|
||||
{
|
||||
if (high is null || low is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (high.Value != low.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return temperature is null || high.Value == temperature.Value;
|
||||
}
|
||||
|
||||
private static string BuildWeatherCacheKey(LocationPoint location, bool useCelsius, int forecastDayOffset)
|
||||
{
|
||||
return string.Create(
|
||||
|
||||
Reference in New Issue
Block a user