Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Guppi.Console/Guppi.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
<Authors>Rob Prouse</Authors>
<Company>Alteridem Consulting</Company>
<Product>Guppi command line utility</Product>
<Copyright>Copyright (c) 2024 Rob Prouse</Copyright>
<Copyright>Copyright (c) 2025 Rob Prouse</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/rprouse/guppi</PackageProjectUrl>
<RepositoryUrl>https://github.com/rprouse/guppi</RepositoryUrl>
<PackageId>dotnet-guppi</PackageId>
<Version>6.5.1</Version>
<Version>6.5.2</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>guppi</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
Expand Down
2 changes: 1 addition & 1 deletion Guppi.Console/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"Guppi.Console": {
"commandName": "Project",
"commandLineArgs": "calendar month"
"commandLineArgs": "weather sunrise"
}
}
}
86 changes: 82 additions & 4 deletions Guppi.Console/Skills/WeatherSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
using Spectre.Console;
using Location = Guppi.Core.Entities.Weather.Location;
using Guppi.Core.Interfaces.Services;
using Guppi.Core.Services;
using Guppi.Core.Configurations;
using Guppi.Core;

namespace Guppi.Console.Skills;

Expand All @@ -33,6 +36,9 @@ public IEnumerable<Command> GetCommands()
var daily = new Command("daily", "Views the daily weather") { location };
daily.Handler = CommandHandler.Create(async (string location) => await Daily(location));

var sunrise = new Command("sunrise", "Views sunrise and sunset times") { location };
sunrise.Handler = CommandHandler.Create(async (string location) => await Sunrise(location));

var configure = new Command("configure", "Configures the weather provider");
configure.AddAlias("config");
configure.Handler = CommandHandler.Create(() => Configure());
Expand All @@ -44,6 +50,7 @@ public IEnumerable<Command> GetCommands()
view,
hourly,
daily,
sunrise,
configure
}
};
Expand All @@ -55,6 +62,16 @@ private async Task<WeatherForecast> GetWeather(string location)
if (string.IsNullOrWhiteSpace(location))
return await _service.GetWeather();

Location selected = await GetLocation(location);

AnsiConsole.MarkupLine($":round_pushpin: [cyan]{selected}[/]");
AnsiConsole.WriteLine();

return await _service.GetWeather(selected.Latitude.ToString(), selected.Longitude.ToString());
}

private async Task<Location> GetLocation(string location)
{
var locations = await _service.GetLocations(location);

if (!locations.Any())
Expand All @@ -75,10 +92,7 @@ private async Task<WeatherForecast> GetWeather(string location)
.AddChoices(locations));
}

AnsiConsole.MarkupLine($":round_pushpin: [cyan]{selected}[/]");
AnsiConsole.WriteLine();

return await _service.GetWeather(selected.Latitude.ToString(), selected.Longitude.ToString());
return selected;
}

private async Task Daily(string location)
Expand Down Expand Up @@ -167,6 +181,70 @@ private async Task Execute(string location, bool all)
}
}

private async Task Sunrise(string location)
{
try
{
Location? selected = null;
if (string.IsNullOrWhiteSpace(location))
{
WeatherConfiguration configuration = Configuration.Load<WeatherConfiguration>("weather");
selected = new Location
{
Name = "Default",
Latitude = configuration.Latitude,
Longitude = configuration.Longitude
};
}
else
{
selected = await GetLocation(location);
}

AnsiConsoleHelper.TitleRule(":sun: Sunrise and Sunset");
AnsiConsoleHelper.TitleRule("Today's daylight", "cyan");

DateTimeOffset now = DateTimeOffset.Now;
double latitude = selected.Latitude.ToDouble();
double longitude = selected.Longitude.ToDouble();
double elevation = 0;
TimeZoneInfo timeZone = TimeZoneInfo.Local;

SunriseResult today = SunriseService.Calculate(now, latitude, longitude, elevation, timeZone);

// Display sunrise/sunset information from the weather forecast
AnsiConsole.MarkupLine($" :sunrise: [yellow]Sunrise:[/] [silver]{today.Sunrise:HH:mm zzz}[/]");
AnsiConsole.MarkupLine($" :sunset: [orange3]Sunset:[/] [silver]{today.Sunset:HH:mm zzz}[/]");
AnsiConsole.MarkupLine($" :eight_o_clock: [aqua]Length:[/] [silver]{today.DayLength.Hours:00} hrs, {today.DayLength.Minutes:00} mins[/]");

AnsiConsole.WriteLine();
AnsiConsoleHelper.TitleRule("Ten day projection", "cyan");

var table = new Table();
table.BorderColor(Color.Blue);
table.Border(TableBorder.Rounded);
table.AddColumn("[blue]Date[/]");
table.AddColumn(new TableColumn("[blue]Sunrise[/]").Centered());
table.AddColumn(new TableColumn("[blue]Sunset[/]").Centered());
table.AddColumn("[blue]Length[/]");

for (int i = 1; i <= 10; i++)
{
DateTimeOffset day = now.AddDays(i);
SunriseResult result = SunriseService.Calculate(day, latitude, longitude, elevation, timeZone);
table.AddRow(day.ToString("ddd MMM dd"), result.Sunrise.ToString("HH:mm"), result.Sunset.ToString("HH:mm"), $"{result.DayLength.Hours:00} hrs, {result.DayLength.Minutes:00} mins");
}
AnsiConsole.Write(table);
AnsiConsole.WriteLine();

AnsiConsoleHelper.Rule("white");
}
catch (UnconfiguredException ue)
{
AnsiConsole.MarkupLine($"[yellow][[:yellow_circle: {ue.Message}]][/]");
}
}

private void Configure() => _service.Configure();

private static void DisplayLong(WeatherForecast weather)
Expand Down
14 changes: 14 additions & 0 deletions Guppi.Core/Entities/Weather/SunriseResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace Guppi.Core.Entities.Weather;

public struct SunriseResult
{
public DateTimeOffset Sunrise { get; set; }
public DateTimeOffset Sunset { get; set; }
public bool IsPolarDay { get; set; }
public TimeSpan DayLength => Sunset - Sunrise;

public override string ToString() =>
IsPolarDay ? "Sun is up all day" : $"Sunrise: {Sunrise}{Environment.NewLine}Sunset: {Sunset}{Environment.NewLine}Length: {DayLength} hours";
}
13 changes: 13 additions & 0 deletions Guppi.Core/Exceptions/MathExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace Guppi.Core.Extensions;

// Extension methods for Math since C# doesn't have Radians/Degrees conversion built-in
public static class MathExtensions
{
public static double Radians(this double degrees) =>
degrees * Math.PI / 180.0;

public static double Degrees(this double radians) =>
radians * 180.0 / Math.PI;
}
13 changes: 13 additions & 0 deletions Guppi.Core/Extensions/MathExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace Guppi.Core.Exceptions;

// Extension methods for Math since C# doesn't have Radians/Degrees conversion built-in
public static class MathExtensions
{
public static double Radians(this double degrees) =>
degrees * Math.PI / 180.0;

public static double Degrees(this double radians) =>
radians * 180.0 / Math.PI;
}
3 changes: 3 additions & 0 deletions Guppi.Core/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public static string StripEmoji(this string str)
return str;
}

public static double ToDouble(this string str) =>
double.TryParse(str, out double result) ? result : 0;

[GeneratedRegex(@"(:[a-z_]+:)", RegexOptions.Compiled)]
private static partial Regex EmojiRegex();
}
Expand Down
110 changes: 110 additions & 0 deletions Guppi.Core/Services/SunriseService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using Guppi.Core.Entities.Weather;
using Guppi.Core.Extensions;

namespace Guppi.Core.Services;

/// <summary>
/// Sunrise and sunset calculator
/// </summary>
/// <remarks>
/// Based on https://en.wikipedia.org/wiki/Sunrise_equation
/// https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth
/// </remarks>
/// <param name="logger"></param>
public static class SunriseService
{
/// <summary>
/// Calculates the sunrise and sunset times for a given date, latitude, longitude, and elevation.
/// </summary>
/// <param name="day">The date for which to calculate the sunrise and sunset times.</param>
/// <param name="latitude">The latitude of the location.</param>
/// <param name="longitude">The longitude of the location.</param>
/// <param name="elevation">The elevation of the location in meters.</param>
/// <param name="debugTz">The time zone to use for debugging purposes.</param>
/// <returns>A <see cref="SunriseResult"/> object containing the sunrise and sunset times, and whether it is a polar day.</returns>
/// <exception cref="ArgumentException">Thrown when the sun is up all day (polar day).</exception>
public static SunriseResult Calculate(
DateTimeOffset day,
double latitude,
double longitude,
double elevation,
TimeZoneInfo debugTz)
{
// Get timestamp (Unix epoch time)
double currentTimestamp = day.ToUnixTimeSeconds();

double jDate = TimestampToJulian(currentTimestamp);

// Julian day
double n = Math.Ceiling(jDate - (2451545.0 + 0.0009) + 69.184 / 86400.0);

// Mean solar time
double j_ = n + 0.0009 - longitude / 360.0;

// Solar mean anomaly
double mDegrees = (357.5291 + 0.98560028 * j_) % 360;
double mRadians = mDegrees.Radians();

// Equation of the center
double cDegrees = 1.9148 * Math.Sin(mRadians) + 0.02 * Math.Sin(2 * mRadians) + 0.0003 * Math.Sin(3 * mRadians);

// Ecliptic longitude
double lDegrees = (mDegrees + cDegrees + 180.0 + 102.9372) % 360;

double lambdaRadians = lDegrees.Radians();

// Solar transit (Julian date)
double jTransit = (2451545.0 + j_ + 0.0053 * Math.Sin(mRadians) - 0.0069 * Math.Sin(2 * lambdaRadians));

// Declination of the Sun
double sinD = Math.Sin(lambdaRadians) * Math.Sin(23.4397.Radians());
double cosD = Math.Cos(Math.Asin(sinD));

// Hour angle
double someCos = (Math.Sin((-0.833 - 2.076 * Math.Sqrt(elevation) / 60.0).Radians()) - Math.Sin(latitude.Radians()) * sinD)
/ (Math.Cos(latitude.Radians()) * cosD);

try
{
double w0Radians = Math.Acos(someCos);
double w0Degrees = w0Radians.Degrees(); // 0...180


double jRise = jTransit - w0Degrees / 360;
double jSet = jTransit + w0Degrees / 360;

return new SunriseResult
{
Sunrise = ConvertUnixToLocalTime(JulianToTimestamp(jRise), debugTz),
Sunset = ConvertUnixToLocalTime(JulianToTimestamp(jSet), debugTz),
IsPolarDay = false
};
}
catch (ArgumentException)
{
return new SunriseResult
{
Sunrise = day.Date,
Sunset = day.AddDays(1).Date,
IsPolarDay = true
};
}
}

private static DateTimeOffset ConvertUnixToLocalTime(double ts, TimeZoneInfo debugTz)
{
DateTimeOffset dateTime = DateTimeOffset.FromUnixTimeSeconds((long)ts);
if (debugTz != null)
{
return TimeZoneInfo.ConvertTime(dateTime, debugTz);
}
return dateTime;
}

public static double JulianToTimestamp(double j) =>
(j - 2440587.5) * 86400;

public static double TimestampToJulian(double ts) =>
ts / 86400.0 + 2440587.5;
}
13 changes: 6 additions & 7 deletions Guppi.Tests/Guppi.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<EnableNUnitRunner>true</EnableNUnitRunner>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<TestingPlatformCaptureOutput>false</TestingPlatformCaptureOutput>
</PropertyGroup>

<ItemGroup>
Expand All @@ -14,12 +17,8 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="nunit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="nunit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading