Skip to content

Commit 6626465

Browse files
authored
AstronomicalCalculator - First step of caching
1 parent 11fd247 commit 6626465

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

‎src/main/java/com/kosherjava/zmanim/util/AstronomicalCalculator.java‎

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import java.time.Instant;
1818
import java.time.LocalDate;
19+
import java.util.concurrent.ConcurrentHashMap;
1920
import java.util.Objects;
2021

2122
/**
@@ -27,6 +28,229 @@
2728
* @author © Eliyahu Hershfeld 2004 - 2026
2829
*/
2930
public abstract class AstronomicalCalculator implements Cloneable {
31+
32+
/**
33+
* Whether this calculator's rise / set / noon / midnight results may be cached (see {@link #isResultCacheable()}). On by default;
34+
* see {@link #setUseResultCache(boolean)}.
35+
*/
36+
private boolean useResultCache = true;
37+
38+
/**
39+
* The cached results for the most recently used dates, newest first, at most {@link #RESULT_CACHE_DATES}. The array is never
40+
* modified after it is published; a new date is added by publishing a new array. Each date's values are held in a concurrent
41+
* map, so a calculator shared between threads stays correct (at most a value is computed twice). Cleared by every setter that
42+
* can change a result, so a cached value is only ever returned for identical inputs and settings. Not part of {@link
43+
* #equals(Object)} or {@link #hashCode()}.
44+
*/
45+
private volatile ResultCache[] resultCaches;
46+
47+
/** Number of dates kept in the result cache. A day's zmanim use one date (and rarely a neighbouring one). */
48+
private static final int RESULT_CACHE_DATES = 3;
49+
50+
/** The calculator method a cached result came from. A rise and a set at the same zenith are different times. */
51+
private enum ResultEvent { SUNRISE, SUNSET, NOON, MIDNIGHT }
52+
53+
/** The cache key: every location input a calculator can use, the zenith, the elevation adjustment and the event. */
54+
private static final class ResultKey {
55+
private final double latitude;
56+
private final double longitude;
57+
private final double elevation;
58+
private final double horizonElevationAtSunrise;
59+
private final double horizonElevationAtSunset;
60+
private final double zenith;
61+
private final boolean adjustForElevation;
62+
private final ResultEvent event;
63+
64+
ResultKey(GeoLocation geoLocation, double zenith, boolean adjustForElevation, ResultEvent event) {
65+
this.latitude = geoLocation.getLatitude();
66+
this.longitude = geoLocation.getLongitude();
67+
this.elevation = geoLocation.getElevation();
68+
this.horizonElevationAtSunrise = geoLocation.getHorizonElevationAtSunrise();
69+
this.horizonElevationAtSunset = geoLocation.getHorizonElevationAtSunset();
70+
this.zenith = zenith;
71+
this.adjustForElevation = adjustForElevation;
72+
this.event = event;
73+
}
74+
75+
@Override
76+
public boolean equals(Object object) {
77+
if (this == object) {
78+
return true;
79+
}
80+
if (!(object instanceof ResultKey)) {
81+
return false;
82+
}
83+
ResultKey other = (ResultKey) object;
84+
return Double.compare(latitude, other.latitude) == 0 && Double.compare(longitude, other.longitude) == 0
85+
&& Double.compare(elevation, other.elevation) == 0
86+
&& Double.compare(horizonElevationAtSunrise, other.horizonElevationAtSunrise) == 0
87+
&& Double.compare(horizonElevationAtSunset, other.horizonElevationAtSunset) == 0
88+
&& Double.compare(zenith, other.zenith) == 0 && adjustForElevation == other.adjustForElevation && event == other.event;
89+
}
90+
91+
@Override
92+
public int hashCode() {
93+
return Objects.hash(latitude, longitude, elevation, horizonElevationAtSunrise, horizonElevationAtSunset, zenith,
94+
adjustForElevation, event);
95+
}
96+
}
97+
98+
/** The cached results of one date. */
99+
private static final class ResultCache {
100+
private final LocalDate date;
101+
private final ConcurrentHashMap<ResultKey, Double> values = new ConcurrentHashMap<ResultKey, Double>();
102+
103+
ResultCache(LocalDate date) {
104+
this.date = date;
105+
}
106+
}
107+
108+
/**
109+
* Whether this calculator's results depend only on the method arguments and on settings whose setters call {@link
110+
* #clearResultCache()}, so they can be cached. Defaults to {@code false}, so a calculator written without the cache in mind is
111+
* never cached. A subclass that returns {@code true} must call {@link #clearResultCache()} from every setter that can change a
112+
* rise, set, noon or midnight result.
113+
* @return whether results may be cached
114+
*/
115+
protected boolean isResultCacheable() {
116+
return false;
117+
}
118+
119+
/**
120+
* Discards all cached results. Called by every setter that can change a result.
121+
*/
122+
protected void clearResultCache() {
123+
resultCaches = null;
124+
}
125+
126+
/**
127+
* Whether rise / set / noon / midnight results are cached (when the calculator supports it, see {@link #isResultCacheable()}).
128+
* Caching never changes a result. A full day of <em>zmanim</em> makes several hundred calculator calls that resolve to a few
129+
* dozen distinct solutions, so the cache removes most of the astronomical work.
130+
* @return whether results are cached. Defaults to {@code true}.
131+
*/
132+
public boolean isUseResultCache() {
133+
return useResultCache;
134+
}
135+
136+
/**
137+
* Sets whether rise / set / noon / midnight results are cached. Caching never changes a result; turning it off is only useful for
138+
* benchmarking, or as a safeguard if a subclass's results depend on state the cache can not see.
139+
* @param useResultCache whether to cache results
140+
*/
141+
public void setUseResultCache(boolean useResultCache) {
142+
this.useResultCache = useResultCache;
143+
clearResultCache();
144+
}
145+
146+
private ConcurrentHashMap<ResultKey, Double> getResultCache(LocalDate localDate) {
147+
ResultCache[] caches = resultCaches;
148+
if (caches != null) {
149+
for (ResultCache cache : caches) {
150+
if (cache.date.equals(localDate)) {
151+
return cache.values;
152+
}
153+
}
154+
}
155+
ResultCache cache = new ResultCache(localDate);
156+
int keep = caches == null ? 0 : Math.min(caches.length, RESULT_CACHE_DATES - 1);
157+
ResultCache[] updated = new ResultCache[keep + 1];
158+
updated[0] = cache;
159+
if (keep > 0) {
160+
System.arraycopy(caches, 0, updated, 1, keep);
161+
}
162+
resultCaches = updated;
163+
return cache.values;
164+
}
165+
166+
private boolean isCaching() {
167+
return useResultCache && isResultCacheable();
168+
}
169+
170+
/**
171+
* {@link #getUTCSunrise(LocalDate, GeoLocation, double, boolean)} with result caching (see {@link #isUseResultCache()}). Used by
172+
* {@link com.kosherjava.zmanim.AstronomicalCalendar}; the result is always identical to the uncached method.
173+
* @param localDate the date
174+
* @param geoLocation the location
175+
* @param zenith the zenith
176+
* @param adjustForElevation whether to adjust for elevation
177+
* @return as {@link #getUTCSunrise(LocalDate, GeoLocation, double, boolean)}
178+
*/
179+
public final double getCachedUTCSunrise(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation) {
180+
if (!isCaching()) {
181+
return getUTCSunrise(localDate, geoLocation, zenith, adjustForElevation);
182+
}
183+
ConcurrentHashMap<ResultKey, Double> cache = getResultCache(localDate);
184+
ResultKey key = new ResultKey(geoLocation, zenith, adjustForElevation, ResultEvent.SUNRISE);
185+
Double value = cache.get(key);
186+
if (value == null) {
187+
value = getUTCSunrise(localDate, geoLocation, zenith, adjustForElevation);
188+
cache.putIfAbsent(key, value);
189+
}
190+
return value;
191+
}
192+
193+
/**
194+
* {@link #getUTCSunset(LocalDate, GeoLocation, double, boolean)} with result caching (see {@link #isUseResultCache()}).
195+
* @param localDate the date
196+
* @param geoLocation the location
197+
* @param zenith the zenith
198+
* @param adjustForElevation whether to adjust for elevation
199+
* @return as {@link #getUTCSunset(LocalDate, GeoLocation, double, boolean)}
200+
*/
201+
public final double getCachedUTCSunset(LocalDate localDate, GeoLocation geoLocation, double zenith, boolean adjustForElevation) {
202+
if (!isCaching()) {
203+
return getUTCSunset(localDate, geoLocation, zenith, adjustForElevation);
204+
}
205+
ConcurrentHashMap<ResultKey, Double> cache = getResultCache(localDate);
206+
ResultKey key = new ResultKey(geoLocation, zenith, adjustForElevation, ResultEvent.SUNSET);
207+
Double value = cache.get(key);
208+
if (value == null) {
209+
value = getUTCSunset(localDate, geoLocation, zenith, adjustForElevation);
210+
cache.putIfAbsent(key, value);
211+
}
212+
return value;
213+
}
214+
215+
/**
216+
* {@link #getUTCNoon(LocalDate, GeoLocation)} with result caching (see {@link #isUseResultCache()}).
217+
* @param localDate the date
218+
* @param geoLocation the location
219+
* @return as {@link #getUTCNoon(LocalDate, GeoLocation)}
220+
*/
221+
public final double getCachedUTCNoon(LocalDate localDate, GeoLocation geoLocation) {
222+
if (!isCaching()) {
223+
return getUTCNoon(localDate, geoLocation);
224+
}
225+
ConcurrentHashMap<ResultKey, Double> cache = getResultCache(localDate);
226+
ResultKey key = new ResultKey(geoLocation, 0, false, ResultEvent.NOON);
227+
Double value = cache.get(key);
228+
if (value == null) {
229+
value = getUTCNoon(localDate, geoLocation);
230+
cache.putIfAbsent(key, value);
231+
}
232+
return value;
233+
}
234+
235+
/**
236+
* {@link #getUTCMidnight(LocalDate, GeoLocation)} with result caching (see {@link #isUseResultCache()}).
237+
* @param localDate the date
238+
* @param geoLocation the location
239+
* @return as {@link #getUTCMidnight(LocalDate, GeoLocation)}
240+
*/
241+
public final double getCachedUTCMidnight(LocalDate localDate, GeoLocation geoLocation) {
242+
if (!isCaching()) {
243+
return getUTCMidnight(localDate, geoLocation);
244+
}
245+
ConcurrentHashMap<ResultKey, Double> cache = getResultCache(localDate);
246+
ResultKey key = new ResultKey(geoLocation, 0, false, ResultEvent.MIDNIGHT);
247+
Double value = cache.get(key);
248+
if (value == null) {
249+
value = getUTCMidnight(localDate, geoLocation);
250+
cache.putIfAbsent(key, value);
251+
}
252+
return value;
253+
}
30254
/**
31255
* The commonly used average solar refraction in degrees that defaults to 34′, or 0.5666°.
32256
* <a href="https://www.cs.tau.ac.il//~nachum/calendar-book/index.shtml">Calendrical Calculations</a> lists a more accurate
@@ -76,6 +300,7 @@ public boolean isUseApparentSolarRadius() {
76300
*/
77301
public void setUseApparentSolarRadius(boolean useApparentSolarRadius) {
78302
this.useApparentSolarRadius = useApparentSolarRadius;
303+
clearResultCache();
79304
}
80305

81306
/**
@@ -114,6 +339,7 @@ public double getEarthRadius() {
114339
*/
115340
public void setEarthRadius(double earthRadius) {
116341
this.earthRadius = earthRadius;
342+
clearResultCache();
117343
}
118344

119345
/**
@@ -347,6 +573,7 @@ public double getRefraction() {
347573
*/
348574
public void setRefraction(double refraction) {
349575
this.refraction = refraction;
576+
clearResultCache();
350577
}
351578

352579
/**
@@ -399,6 +626,7 @@ public void setSolarRadius(double solarRadius) {
399626
}
400627
this.solarRadius = solarRadius;
401628
this.useApparentSolarRadius = false;
629+
clearResultCache();
402630
}
403631

404632
/**

0 commit comments

Comments
 (0)