Reputation: 833
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));
}
Upvotes: 3
Views: 664
Reputation: 29028
I assume this probably can be solved with a
reduce()
It's a perfectly valid use case for reduce()
.
I have one proposal regarding the structure of ForecastPerDate
. It would be far more convenient to store the date as LocalDate
and not as String
. That is the correct way of dealing with dates, because there's a little you can do with a string representing the date without parsing.
And even if you don't consider the idea of changing the ForecastPerDate
as a possible option, it would not be difficult to adopt the following approach.
I'll proceed with ForecastPerDate
record defined as follows:
public record ForecastPerDate(LocalDate date, Double value) {}
Find items that have
date >= today
When we need to compare the two LocalDate
objects there's no need to extract day from epoch and manipulate with them manually, basically it's a violation of the Information expert principle because LocalDate
instances are capable to compare their data. Just let them do that by using ifAfter()
and equals()
.
And there's no need for the intermediate Map
. We can perform the reduction on the ForecastPerDate
objects by always picking the one with the closest upcoming date.
In the code below, reduce()
produces the result as an Optional<ForecastPerDate>
(because identity is not provided) which gets transformed into Optional<Long>
. If optional is empty, then a default value of 0L
will be provided.
That's how it might look like:
public static Long getAvailabilityFromForecastData(final String fuCode,
final String articleCode,
final Collection<ForecastPerDate> forecasts) {
// writing a log message
LocalDate now = LocalDate.now();
Long mostClosestForecastValue = forecasts.stream()
.filter(forecast -> forecast.date().isAfter(now) || forecast.date.equals(now)) // today in the future
.filter(forecast -> forecast.value() != null)
.reduce((result, next) -> result.date().isBefore(next.date()) ? result : next) // Optional<ForecastPerDate>
.map(forecast -> availabilityAmountFormatter(forecast.value())) // Optional<Long>
.orElse(0L); // extracting Long value from the optional
// writing a log message
return mostClosestForecastValue;
}
Upvotes: 3
Reputation: 40062
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());
}
Upvotes: 0
Reputation: 4707
If you want to keep a similar flow to what you have, try something like this:
final LocalDate now = LocalDate.now();
return forecasts.stream()
.map(ForecastPerDate::date)
.map(LocalDate::parse)
.filter(fd -> fd.isEqual(now) || fd.isAfter(now))
.min()
.map(LocalDate::toString)
.map(forecastMap::get)
.map(this::availabilityAmountFormatter)
.orElseGet(() -> {
log.info("Resorting to fallback...");
return 0L;
});
Upvotes: -1
Reputation: 28056
Most of the operations you mention are available method-calls on streams.
forecasts.stream()
.filter()
.min()
, giving an Optional<ForecastPerDate>
optional.map()
optional.orElseGet()
Put together, it would be something like this (I haven't compiled it, so it probably won't work on the first try):
@Override
public Long getAvailabilityFromForecastData(final String fuCode,
final String articleCode,
final Collection<ForecastPerDate> forecasts) {
var today = LocalDate.now();
return forecasts.stream()
.filter(forecast -> !today.isBefore(LocalDate.parse(forecast.date())))
.min(Comparator.comparing(forecast ->
Duration.between(today, LocalDate.parse(forecast.date()))
.map(forecast -> availabilityAmountFormatter(forecast.value()))
.orElseGet(() -> {
log.info("No forecasts found");
return 0L;
});
}
I would move some of the logic into ForecastPerDate
to avoid having to parse forecast.date()
multiple times.
Upvotes: 1