Tapan
Tapan

Reputation: 187

Custom Sort using Java 8

Consider the dto class below -

public class RoleDto {

  private String name;

  RoleDto(String name) {
    this.name = name;
  }
}

Its instances would contain names like -

Business Practitioner - Expert
Developer - Expert
Developer - Master
Business Practitioner - Master
Business Practitioner
Developer
Developer - Professional
Business Practitioner - Professional 

I want to sort the instances such that the order is -

Business Practitioner - Expert
Business Practitioner - Master
Business Practitioner - Professional
Business Practitioner

Developer - Expert
Developer - Master
Developer - Professional
Developer

I have used the code below -

ArrayList<RoleDto> roles = new ArrayList<>();
roles.add(new RoleDto("Business Practitioner - Expert"));
roles.add(new RoleDto("Developer - Expert"));
roles.add(new RoleDto("Developer - Master"));
roles.add(new RoleDto("Business Practitioner - Master"));
roles.add(new RoleDto("Business Practitioner"));
roles.add(new RoleDto("Developer"));
roles.add(new RoleDto("Developer - Professional"));
roles.add(new RoleDto("Business Practitioner - Professional"));

List<RoleDto> sorted = roles.stream().sorted(comparing(r -> r.getName(),
    comparing((String s) -> !s.contains("-")))).collect(toList());

for(RoleDto r : sorted)
System.out.println(r.getName());

However the output is

Business Practitioner - Expert
Developer - Expert
Developer - Master
Business Practitioner - Master
Developer - Professional
Business Practitioner - Professional
Business Practitioner
Developer

Can someone please help me achieve the expected result

Upvotes: 0

Views: 1540

Answers (3)

Basil Bourque
Basil Bourque

Reputation: 338336

It seems you have two dimensions of data at play here.

  • Business function
    "Business Practitioner" & "Programmer".
  • Level of proficiency
    "Expert", "Master", "Professional", and implicitly "Standard", "Basic", "Novice", or some such (shown as no text).

Trying to represent such values as text is clumsy, and ignores the features built into Java for such a purpose.

If all the values are known at compile-time, make an enum for each dimension. An enum is a class defined in such a way as to automatically instantiate and name several objects when the class is loaded.

public enum Function { BUSINESS_PRACTITIONER, PROGRAMMER ; }
public enum Proficiency { EXPERT, MASTER, PROFESSIONAL, NOVICE; }

Pull those together into a single class. The primary purpose of this class is to carry data transparently and immutably. So we can define briefly as a record.

public record Role( Function function , Proficiency proficiency ) { }

We want instances of Role to sort in two levels, first by Function, then by Proficiency. So we need to implement the Comparable interface on our record Role.

public record Role( Function function , Proficiency proficiency ) implements Comparable<Role> { … }

The sort order of each is the order in which the named objects are declared. So we can write the necessary compareTo method using a Comparator object. We define that Comparator object using convenience methods comparing and thenComparing, passing each a method reference. We mark this object as a singleton using static, for frequent re-use.

private static Comparator < Role > comparator =
        Comparator
                .comparing( Role :: function )
                .thenComparing( Role :: proficiency );

We use that comparator in our compareTo method.

@Override public int compareTo ( Role other ) { return Role.comparator.compare( this , other ); }

We instantiate a role object like this:

Role progPro = new Role( Function.PROGRAMMER , Proficiency.PROFESSIONAL );

If need be, we can make a set of all possible roles.

NavigableSet < Role > allPossibleRoles = new TreeSet();
for ( Function function : Function.values() )
{
    for ( Proficiency proficiency : Proficiency.values() )
    {
        allPossibleRoles.add( new Role( function , proficiency ) );
    }
}
System.out.println( "allPossibleRoles = " + allPossibleRoles );

We can see these are listed in the order dictated by the Question.

allPossibleRoles = [Role[function=BUSINESS_PRACTITIONER, proficiency=EXPERT], Role[function=BUSINESS_PRACTITIONER, proficiency=MASTER], Role[function=BUSINESS_PRACTITIONER, proficiency=PROFESSIONAL], Role[function=BUSINESS_PRACTITIONER, proficiency=NOVICE], Role[function=PROGRAMMER, proficiency=EXPERT], Role[function=PROGRAMMER, proficiency=MASTER], Role[function=PROGRAMMER, proficiency=PROFESSIONAL], Role[function=PROGRAMMER, proficiency=NOVICE]]

Lastly, we need to emit the text seen in the Question.

Modify the enum classes to take an argument for a display name.

public enum Function
{
    BUSINESS_PRACTITIONER( "Business Practitioner" ), PROGRAMMER( "Programmer" );

    private final String displayName;

    // Constructor
    Function ( final String displayName ) { this.displayName = displayName; }

    public String getDisplayName ( ) { return displayName; }
}

And the other enum.

public enum Proficiency
{
    EXPERT( "Expert" ), MASTER( "Master" ), PROFESSIONAL( "Professional" ), NOVICE( "" );

    private final String displayName;

    // Constructor
    Proficiency ( String displayName ) { this.displayName = displayName; }

    public String getDisplayName ( ) { return displayName; }
}

Modify the Role record to generate text in desired format with a getDisplayName method.

import java.util.Comparator;

public record Role( Function function , Proficiency proficiency ) implements Comparable < Role >
{
    @Override
    public int compareTo ( Role other ) { return Role.comparator.compare( this , other ); }

    private static Comparator < Role > comparator =
            Comparator
                    .comparing( Role :: function )
                    .thenComparing( Role :: proficiency );

    public String getDisplayName ( )
    {
        String x = this.function.getDisplayName();
        String y = this.proficiency.getDisplayName().isBlank() ? "" : " - ";
        String z = this.proficiency.getDisplayName();
        return x + y + z;
    }
}

Example usage.

NavigableSet < Role > allPossibleRoles = new TreeSet();  // NavigableSet/TreeSet keeps elements sorted.
for ( Function function : Function.values() )
{
    for ( Proficiency proficiency : Proficiency.values() )
    {
        allPossibleRoles.add( new Role( function , proficiency ) );
    }
}

// Dump to console.
for ( Role role : allPossibleRoles )
{
    System.out.println( "role.getDisplayName() = " + role.getDisplayName() );
}
role.getDisplayName() = Business Practitioner - Expert
role.getDisplayName() = Business Practitioner - Master
role.getDisplayName() = Business Practitioner - Professional
role.getDisplayName() = Business Practitioner
role.getDisplayName() = Programmer - Expert
role.getDisplayName() = Programmer - Master
role.getDisplayName() = Programmer - Professional
role.getDisplayName() = Programmer

Upvotes: 1

Andreas Berheim Brudin
Andreas Berheim Brudin

Reputation: 2280

You do not actually compare the names, the compare just checks whether or not the names contain "-", which is why you are getting all that do (in random order) before all that don't (in random order). If you can change the Dto, see the answer of @Orr, otherwise you could do the name split/compare in place, like this:

    final List<RoleDto> sorted =
        roles.stream()
            .sorted(
                comparing(
                    RoleDto::getName,
                    comparing((String s) -> s.split(" - ")[0])
                        .reversed()
                        .thenComparing(s -> s.split(" - ").length)
                        .reversed()
                        .thenComparing(s -> s.split(" - ")[1])))
            .collect(toList());

The reversing may seem a little unintuitive, but effectively every reverse reverses all the previous comparisons, so the first comparison get reversed twice. To skip that, you could instead write:

    final List<RoleDto> sorted =
        roles.stream()
            .sorted(
                comparing(
                    RoleDto::getName,
                    comparing((String s) -> s.split(" - ")[0])
                        .thenComparing(s -> -s.split(" - ").length)
                        .thenComparing(s -> s.split(" - ")[1])))
            .collect(toList());

(notice the minus sign in the middle compare)

Upvotes: 1

Orr Benyamini
Orr Benyamini

Reputation: 405

I would suggest changing your RoleDto to have two attributes: String role and String level

Now the code for sorting List would look like:

Comparator<RoleDto> compareByRoleAndLevel = Comparator
                    .comparing(RoleDto::getRole)
                    .thenComparing(RoleDto::getLevel);
 
List<RoleDto> sortedRoles = roles.stream()
                .sorted(compareByRoleAndLevel)
                .collect(Collectors.toList());

Upvotes: 3

Related Questions