Home > Net >  Java find closest value by property with Streams API
Java find closest value by property with Streams API

Time:06-16

I come asking a question that I feel has not been exactly asked before, and it might produce some interesting answers :)

I am currently working on some Java code that has as goal:

  • Receive list of Collection<ForecastPerDate> (see below)
  • Find items that have date >= today
  • Get the "value" of the item with date closest to today (minimum diff)
  • Floor it and round it
  • If no data has been found it should fallback to 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));
    }

Thanks in advance! Look forward to learning and constructive feedback!

CodePudding user response:

Most of the operations you mention are available method-calls on streams.

  • Receive list of Collection -> forecasts.stream()
  • Find items that have date >= today -> .filter()
  • Get the "value" of the item with date closest to today (minimum diff) -> .min(), giving an Optional<ForecastPerDate>
  • Floor it and round it -> optional.map()
  • If no data has been found it should fallback to 0 with a log message -> 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.

CodePudding user response:

Here is one approach.

  • stream the collection of objects
  • filter out any dates older than and including today.
  • then collect using Collectors.minBy and a special comparator.
  • then use the result with the rest of your code to either return the value or log the result.
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.

CodePudding user response:

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;
        });
  • Related