I am currently working on some Java code that has a goal:
Collection<ForecastPerDate>
(see below);date >= today
;value
of the item with date closest to today (minimum diff
);0
with a log message.public record ForecastPerDate(String date, Double value) {}
My implementation so far seems pretty efficient and sane to me, but I don't like mutating variables or state (I am becoming more of a Haskell dev lately haha) and always quite liked using the Streams API of Java.
Just FYI the project uses Java 17 so that helps. I assume this probably can be solved with a reduce()
function and some accumulator but I am unclear on how to, at least without causing more than one iteration.
Here is the code:
@Override
public Long getAvailabilityFromForecastData(final String fuCode,
final String articleCode,
final Collection<ForecastPerDate> forecasts) {
if (forecasts == null || forecasts.isEmpty()) {
log.info(
"No forecasts received for FU {} articleCode {}, assuming 0!",
fuCode,
articleCode
);
return 0L;
}
final long todayEpochDay = LocalDate.now().toEpochDay();
final Map<String, Double> forecastMap = new HashMap<>();
long smallestDiff = Integer.MAX_VALUE;
String smallestDiffDate = null;
for (final ForecastPerDate forecast : forecasts) {
final long forecastEpochDay = LocalDate.parse(forecast.date()).toEpochDay();
final long diff = forecastEpochDay - todayEpochDay;
if (diff >= 0 && diff < smallestDiff) {
// we look for values in present or future (>=0)
smallestDiff = diff;
smallestDiffDate = forecast.date();
forecastMap.put(forecast.date(), forecast.value());
}
}
if (smallestDiffDate != null) {
final Double wantedForecastValue = forecastMap.get(smallestDiffDate);
if (wantedForecastValue != null) {
return availabilityAmountFormatter(wantedForecastValue);
}
}
log.info(
"Resorting to fallback for FU {} articleCode {}, 0 availability for article! Forecasts: {}",
fuCode,
articleCode,
forecasts
);
return 0L;
}
private Long availabilityAmountFormatter(final Double raw) {
return Math.round(Math.floor(raw));
}
EDIT: In the end after all suggestions here, a nice little algorithm came out:
private static Long toEpochDay(final String date) {
return LocalDate.parse(date).toEpochDay();
}
@Override
public Long getAvailabilityFromForecastData(final String fuCode,
final String articleCode,
final Collection<ForecastPerDate> forecasts) {
final long today = LocalDate.now().toEpochDay();
final String fallbackMessage = "Resorting to fallback for FU {} articleCode {},"
+ " 0 availability for article! Forecasts: {}";
if (forecasts == null) {
log.info(fallbackMessage, fuCode, articleCode, null);
return 0L;
}
final Optional<ForecastPerDate> result = forecasts.stream()
.filter(fpd -> toEpochDay(fpd.date()) > today)
.min(Comparator.comparing(fpd -> toEpochDay(fpd.date()) - today));
if (result.isPresent()) {
return availabilityAmountFormatter(result.get().value());
} else {
log.info(fallbackMessage, fuCode, articleCode, forecasts);
return 0L;
}
}
private Long availabilityAmountFormatter(final Double raw) {
return Math.round(Math.floor(raw));
}
Here is one approach.
Collectors.minBy
and a special comparator.public Long getAvailabilityFromForecastData(final String fuCode,
final String articleCode,
final Collection<ForecastPerDate> forecasts) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("M/d/yyyy");
Comparator<ForecastPerDate> comp =
Comparator.comparing(f -> LocalDate.parse(f.date(), dtf),
(date1, date2) -> date1.compareTo(date2));
Optional<ForecastPerDate> result = forecasts.stream()
.filter(fpd -> LocalDate.parse(fpd.date(),dtf)
.isAfter(LocalDate.now()))
.collect(Collectors.minBy(comp));
if (result.isPresent()) {
return availabilityAmountFormatter(result.get().value());
}
log.info(
"Resorting to fallback for FU {} articleCode {}, 0 availability for article! Forecasts: {}",
fuCode, articleCode, forecasts);
return 0L;
}
Demo
Note: for this answer and demo I included in the above method a DateTimeFormatter
since I don't know the format of your dates. You will probably need to alter it for your application.
List<ForecastPerDate> list = List.of(
new ForecastPerDate("6/14/2022", 112.33),
new ForecastPerDate("6/19/2022", 122.33),
new ForecastPerDate("6/16/2022", 132.33),
new ForecastPerDate("6/20/2022", 142.33));
long v = getAvailabilityFromForecastData("Foo","Bar", list);
System.out.println(v);
prints
132 (based on current day of 6/15/2022)
If no dates are present after the current day, 0
will be returned and the issue logged.
Updated
I believe this will be somewhat more efficient since the dates only need to be parsed a single time.
You can put date and the actual ForecastPerDate
object in an AbstractMap.SimpleEntry
and at the same time parse the LocalDate
.
map(fpd -> new SimpleEntry<>( LocalDate.parse(fpd.date(), dtf), fpd))
minBy
Comparator is also simpler since there are pre-existing Entry comparators for key and value. So use
.collect(Collectors.minBy(Entry.comparingByKey()));
Putting it all together.
Optional<Entry<LocalDate, ForecastPerDate>> result = forecasts
.stream()
.map(fpd -> new SimpleEntry<>(
LocalDate.parse(fpd.date(), dtf), fpd))
.filter(e -> e.getKey().isAfter(LocalDate.now()))
.collect(Collectors.minBy(Entry.comparingByKey()));
To get the result there is one extra level of indirection. Get the Optional contents followed by the Entry.value()
and then the value
from ForecastPerDate
object.
if (result.isPresent()) {
return availabilityAmountFormatter(result.get().getValue().value());
}