blob: f93f49ed9beeddf3b70d282931bdde8c26012e6d [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/time/astronomer_util.h"
#include <cmath>
#include <ctime>
#include <string>
#include "base/logging.h"
#include "base/time/time.h"
#include "third_party/icu/source/i18n/astro.h"
namespace ash {
namespace {
base::expected<base::Time, SunRiseSetError> GetTime(
base::Time::Exploded& exploded,
double hours_in_day) {
// We only need minutes level precision.
exploded.hour = std::floor(hours_in_day);
exploded.minute = fmod(hours_in_day * 60, 60);
exploded.second = 0;
exploded.millisecond = 0;
base::Time ret;
if (!base::Time::FromUTCExploded(exploded, &ret)) {
return base::unexpected(SunRiseSetError::kUnavailable);
}
return base::ok(ret);
}
} // namespace
// The most of this function was generated by Gemini.
base::expected<SunRiseSetTime, SunRiseSetError>
GetSunriseSunset(const base::Time& time, double latitude, double longitude) {
base::Time::Exploded exploded;
time.UTCExplode(&exploded);
std::tm t = {};
t.tm_year = exploded.year - 1900;
t.tm_mon = exploded.month - 1;
t.tm_mday = exploded.day_of_month;
std::mktime(&t);
int day_of_year = t.tm_yday + 1;
auto to_radians = [](double degrees) { return degrees * M_PI / 180.0; };
auto to_degrees = [](double radians) { return radians * 180.0 / M_PI; };
// Zenith for sunrise/sunset calculation (official is 90 degrees 50 minutes)
const double zenith = 90.8333;
// 2. Convert the longitude to hour value and calculate an approximate time
double lng_hour = longitude / 15.0;
double t_rise = day_of_year + ((6.0 - lng_hour) / 24.0);
double t_set = day_of_year + ((18.0 - lng_hour) / 24.0);
// 3. Calculate the Sun's mean anomaly
double m_rise = (0.9856 * t_rise) - 3.289;
double m_set = (0.9856 * t_set) - 3.289;
// 4. Calculate the Sun's true longitude
double l_rise = m_rise + (1.916 * sin(to_radians(m_rise))) +
(0.020 * sin(to_radians(2 * m_rise))) + 282.634;
double l_set = m_set + (1.916 * sin(to_radians(m_set))) +
(0.020 * sin(to_radians(2 * m_set))) + 282.634;
l_rise = fmod(l_rise, 360.0);
l_set = fmod(l_set, 360.0);
// 5a. Calculate the Sun's right ascension
double ra_rise = to_degrees(atan(0.91764 * tan(to_radians(l_rise))));
double ra_set = to_degrees(atan(0.91764 * tan(to_radians(l_set))));
ra_rise = fmod(ra_rise, 360.0);
ra_set = fmod(ra_set, 360.0);
// 5b. Right ascension value needs to be in the same quadrant as L
double l_quadrant_rise = floor(l_rise / 90.0) * 90.0;
double ra_quadrant_rise = floor(ra_rise / 90.0) * 90.0;
ra_rise = ra_rise + (l_quadrant_rise - ra_quadrant_rise);
double l_quadrant_set = floor(l_set / 90.0) * 90.0;
double ra_quadrant_set = floor(ra_set / 90.0) * 90.0;
ra_set = ra_set + (l_quadrant_set - ra_quadrant_set);
// 5c. Right ascension value needs to be converted into hours
double ra_rise_hours = ra_rise / 15.0;
double ra_set_hours = ra_set / 15.0;
// 6. Calculate the Sun's declination
double sin_dec_rise = 0.39782 * sin(to_radians(l_rise));
double cos_dec_rise = cos(asin(sin_dec_rise));
double sin_dec_set = 0.39782 * sin(to_radians(l_set));
double cos_dec_set = cos(asin(sin_dec_set));
// 7a. Calculate the Sun's local hour angle
double cos_h_rise =
(cos(to_radians(zenith)) - (sin_dec_rise * sin(to_radians(latitude)))) /
(cos_dec_rise * cos(to_radians(latitude)));
double cos_h_set =
(cos(to_radians(zenith)) - (sin_dec_set * sin(to_radians(latitude)))) /
(cos_dec_set * cos(to_radians(latitude)));
if (cos_h_rise > 1.0) {
return base::unexpected(SunRiseSetError::kNoSunRiseSet);
}
if (cos_h_set < -1.0) {
return base::unexpected(SunRiseSetError::kNoSunRiseSet);
}
// 7b. Finish calculating H and convert into hours
double h_rise_deg = 360.0 - to_degrees(acos(cos_h_rise));
double h_set_deg = to_degrees(acos(cos_h_set));
double h_rise_hours = h_rise_deg / 15.0;
double h_set_hours = h_set_deg / 15.0;
// 8. Calculate local mean time of rising/setting
double t_local_rise =
h_rise_hours + ra_rise_hours - (0.06571 * t_rise) - 6.622;
double t_local_set = h_set_hours + ra_set_hours - (0.06571 * t_set) - 6.622;
// 9. Adjust back to UTC
double ut_rise = t_local_rise - lng_hour;
double ut_set = t_local_set - lng_hour;
// Normalize to 0-24
double sunrise_utc_hours = fmod(ut_rise + 24.0, 24.0);
double sunset_utc_hours = fmod(ut_set + 24.0, 24.0);
SunRiseSetTime result;
auto sunrise = GetTime(exploded, sunrise_utc_hours);
if (!sunrise.has_value()) {
return base::unexpected(sunrise.error());
}
result.sunrise = *sunrise;
auto sunset = GetTime(exploded, sunset_utc_hours);
if (!sunset.has_value()) {
return base::unexpected(sunset.error());
}
result.sunset = *sunset;
// Sunrise time may be 1 day earlier if the time was normalized to 24 hours.
if (result.sunset < result.sunrise) {
result.sunset += base::Days(1);
}
return result;
}
base::expected<SunRiseSetTime, SunRiseSetError>
GetSunriseSunsetICU(const base::Time& time, double latitude, double longitude) {
icu::CalendarAstronomer astro(longitude, latitude);
astro.setTime(time.InMillisecondsFSinceUnixEpoch());
const double sun_rise_ms = astro.getSunRiseSet(/*sunrise=*/true);
if (sun_rise_ms < 0) {
return base::unexpected(SunRiseSetError::kNoSunRiseSet);
}
const double sun_set_ms = astro.getSunRiseSet(/*sunrise=*/false);
if (sun_set_ms < 0) {
return base::unexpected(SunRiseSetError::kNoSunRiseSet);
}
return base::ok(SunRiseSetTime{
.sunrise = base::Time::FromMillisecondsSinceUnixEpoch(sun_rise_ms),
.sunset = base::Time::FromMillisecondsSinceUnixEpoch(sun_set_ms)});
}
} // namespace ash