Sam Spain
Sam Spain

Reputation: 113

Sort List By Variable List Of Criteria In Java

I've made a small Spring boot service that allows the ability to filter and sort a list of objects, with the sorting able to be defined in any order.

The object only has a few values within it: name (string), height (double), category (string), and grid reference (string)

I have converted the API arguments for sorting into a list of String arguments. An example would be ["category, "desc", height, "asc"] "Sort by category descending, and then height ascending"

Given how few values there are I decided to first get my test passing by using a switch statement by matching the string values to an enum.

The solution code

List<Munro> findData(MunroRequest munroRequest) {
       List<Munro> munroData = munroRepository.find();
        Stream<Munro> munroStream = munroData.stream().filter(munro -> munro.getHeight() <= munroRequest.getMaxHeight())
                .filter(munro -> munro.getHeight() >= munroRequest.getMinHeight());
        if(!munroRequest.getCategoryFilter().equals(MunroCategoryFilter.ALL)) {
            munroStream = munroStream.filter(munro -> munro.getCategory().equals(munroRequest.getCategoryFilter().name()));
        }
        munroStream = sort(munroRequest, munroStream);
        return munroStream.limit(munroRequest.getMaxResults()).collect(Collectors.toList());
    }

    private Stream<Munro> sort(MunroRequest munroRequest, Stream<Munro> munroStream) {
        Comparator<Munro> munroComparator = Comparator.comparing(Munro::getGridReference);
        for (int i = 0; i < munroRequest.getMunroSorts().size() ; i++ ) {
            switch (munroRequest.getMunroSorts().get(i).getFieldToSort()) {
                case NAME:
                    munroComparator = munroComparator.thenComparing(Munro::getName);
                    break;
                case HEIGHT:
                    munroComparator = munroComparator.thenComparing(Munro::getHeight);
                    break;
                case CATEGORY:
                    munroComparator = munroComparator.thenComparing(Munro::getCategory);
                    break;
                case GRID_REFERENCE:
                    munroComparator = munroComparator.thenComparing(Munro::getGridReference);
                    break;
                default:
                    throw new RuntimeException();
            }
            if(!munroRequest.getMunroSorts().get(i).isAscending()) {
                munroComparator = munroComparator.reversed();
            }
        }

        return munroStream.sorted(munroComparator);
    }

My test code for sorting:

@Test
    void sortResults() {
        MunroRequest sortRequest = new MunroRequest.Builder().withSorting(Arrays.asList(MunroSortingFields.CATEGORY.getParam, "desc", MunroSortingFields.HEIGHT.getParam, "asc")).build();
        List<Munro> munros = makeMunros();
        when(mockedRepository.find()).thenReturn(munros);
        List<Munro> sortedMunro = munroService.findData(sortRequest);
        assertEquals("TOP", sortedMunro.get(0).getCategory());
        assertEquals(45.5D, sortedMunro.get(1).getHeight());
        assertEquals(45.6D, sortedMunro.get(2).getHeight());
        assertEquals(60.6D, sortedMunro.get(3).getHeight());
    }

    private List<Munro> makeMunros() {
        Munro otherMunroToFilter = new Munro("Charlie", 45.6D, "MUN", "001233");
        Munro munroToInclude = new Munro("Sam", 45.5D, "MUN", "001234");
        Munro munroTOFilter = new Munro("Bill", 60.5D, "TOP", "001235");
        Munro largestMunro = new Munro("Dillinger", 60.6D, "MUN", "001232");
        return Arrays.asList(munroToInclude, munroTOFilter, otherMunroToFilter, largestMunro);
    }

Whole code can be found here but I think I've included what's important.

The thing is I'm really unhappy with this solution. I've been trying to look for a solution where I could avoid a switch statement as the information of mapping these arguments to fields is done in quite a verbose way.

I was hoping some sort of mapping of string to comparator function could be done elsewhere but haven't figured out what I could do. Using Reflection would acceptable but ideally avoided but I'm not sure how that could be done either.

I don't really make many questions but I genuinely couldn't find any similar question to this with a variable set of sorting criteria.

Upvotes: 1

Views: 529

Answers (1)

Jo&#227;o Dias
Jo&#227;o Dias

Reputation: 17460

You can add the mapping function to the Enumeration and thus get rid of the switch statement.

You MunroSortingFields enum would become:

public enum MunroSortingFields {

    NAME("name", Munro::getName),
    HEIGHT("height", Munro::getHeight),
    CATEGORY("category", Munro::getCategory),
    GRID_REFERENCE("grid_reference", Munro::getGridReference);

    String getParam;

    Function<? super Munro, ? extends Comparable> comparingFunction;

    MunroSortingFields(String getParam, Function<? super Munro, ? extends Comparable> comparingFunction) {
        this.getParam = getParam;
        this.comparingFunction = comparingFunction;
    }

    public static Optional<MunroSortingFields> fromString(String getParam) {
        return Arrays.stream(MunroSortingFields.values()).filter(field -> field.getParam.equals(getParam)).findFirst();
    }
}

And your sort() method would become way simpler:

private Stream<Munro> sort(MunroRequest munroRequest, Stream<Munro> munroStream) {
    Comparator<Munro> munroComparator = Comparator.comparing(Munro::getGridReference);
    for (int i = 0; i < munroRequest.getMunroSorts().size() ; i++ ) {
        munroComparator = munroComparator.thenComparing(munroRequest.getMunroSorts().get(i).getFieldToSort().comparingFunction);

        if(!munroRequest.getMunroSorts().get(i).isAscending()) {
            munroComparator = munroComparator.reversed();
        }
    }

    return munroStream.sorted(munroComparator);
}

Upvotes: 1

Related Questions