+ Start a Discussion
nwallacenwallace 

Sort List of sObjects by Field Value

Hi,

 

There is an old thread here that presents some code to sort a list of sObjects by a field value without using ORDER BY in your query.

 

I recently wanted to do this very thing, so I took a look at that code, but I wasn't satisfied.  I couldn't read the code he presented.  Honestly, it probably works just fine, but since I couldn't read it, I didn't want to use it (and potentially have to maintain it over time).  So I made my own.

 

My implementation is much cleaner and easier to read.  All you have to give it is your List<sObject> and a String representing the field you want to sort on.  It handles Strings, Numbers, and Time type fields, as defined below:

    Strings:  Email, Id, Phone, Picklist (single-select), Reference (Id), String, URL

    Numbers:  Floating types, Integer, Percent

    Time:  Date, Datetime, Time

 

If the field is not of any of those types, the method throws an exception.  It will also fail if the field isn't included on all of the objects in the list.

 

The hard and potentially confusing part of this code is the dynamic behavior it requires to use a string (fieldName) to compare actual values on sObject records.  In the sortByField method, I retrieve the Schema.sObjectField that gets passed through the sort algorithm and used in the comparisons.  The comparison methods need this field to determine the field type so it will do the right kind of comparison (String, Number, or Time).

 

I use quicksort, because a related blog post already had it implemented for me, but you could easily swap out the algorithm for heapsort or something else guaranteed to be nlogn.

 

Here is the code!  I'm very open to suggestions/improvements, so leave a comment if you've got any improvements or if you find any bugs.  I hope this is useful to someone out there!

 

/*
Sorts a list of sObjects, sorted by the value of the specified field
Throws: SortingException
*/
public static void sortByField(List<sObject> objs, String fieldName) {
	if (objs.size() < 2) return;
	Schema.sObjectField field = objs.getsObjectType().getDescribe().fields.getMap().get(fieldName);
if (field == null) throw new SortingException('No such field ' + fieldName); Schema.DescribeFieldResult fieldDesc = field.getDescribe(); if (!fieldDesc.isSortable()) throw new SortingException('Type not sortable: ' + fieldDesc.getType()); quicksortByField(objs, 0, objs.size()-1, field, fieldDesc.gettype()); }
public class SortingException extends Exception {}
/* Implements quicksort on the list of sObjects given */ private static void quicksortByField(List<sObject> a, Integer lo0, Integer hi0, Schema.sObjectField field, Schema.DisplayType type) { Integer lo = lo0; Integer hi = hi0; if (lo >= hi) { return; } else if (lo == hi - 1) { if (compareFields(a[lo], a[hi], field, type) > 0) { sObject o = a[lo]; a[lo] = a[hi]; a[hi] = o; } return; } sObject pivot = a[(lo + hi) / 2]; a[(lo + hi) / 2] = a[hi]; a[hi] = pivot; while (lo < hi) { while (compareFields(a[lo], pivot, field, type) < 1 && lo < hi) { lo++; } while (compareFields(pivot, a[hi], field, type) < 1 && lo < hi) { hi--; } if (lo < hi) { sObject o = a[lo]; a[lo] = a[hi]; a[hi] = o; } } a[hi0] = a[hi]; a[hi] = pivot; quicksortByField(a, lo0, lo-1, field, type); quicksortByField(a, hi+1, hi0, field, type); }
/* Determines the type of primitive the field represents, then returns the appropriate comparison */ private static Integer compareFields(sObject a, sObject b, Schema.sObjectField field, Schema.DisplayType type) { if (type == Schema.DisplayType.Email || type == Schema.DisplayType.Id || type == Schema.DisplayType.Phone || type == Schema.DisplayType.Picklist || type == Schema.DisplayType.Reference || type == Schema.DisplayType.String || type == Schema.DisplayType.URL) { // compareTo method does the same thing as the compare methods below for Numbers and Time // compareTo method on Strings is case-sensitive. Use following line for case-sensitivity
// return String.valueOf(a.get(field)).compareTo(String.valueOf(b.get(field)));
return String.valueOf(a.get(field)).toLowerCase().compareTo(String.valueOf(b.get(field)).toLowerCase()); } else if (type == Schema.DisplayType.Currency || type == Schema.DisplayType.Double || type == Schema.DisplayType.Integer || type == Schema.DisplayType.Percent) { return compareNumbers(Double.valueOf(a.get(field)), Double.valueOf(b.get(field))); } else if (type == Schema.DisplayType.Date || type == Schema.DisplayType.DateTime || type == Schema.DisplayType.Time) { return compareTime(Datetime.valueOf(a.get(field)), Datetime.valueOf(b.get(field))); } else { throw new SortingException('Type not sortable: ' + type); } }
private static Integer compareNumbers(Double a, Double b) { if (a < b) { return -1; } else if (a > b) { return 1; } else { return 0; } }
private static Integer compareTime(Datetime a, Datetime b) { if (a < b) { return -1; } else if (a > b) { return 1; } else { return 0; } }

 

 

Update 2/26/2013:

  • Discovered sort of a bug with String comparisons:  using String.compareTo is case-sensitive.  I personally would prefer a case-insensitive comparison, so I have modified the code to do so.  If you prefer case-sensitivity, you can uncomment the original line of code and use that instead.
  • Since the method is destructive to the original list (changes the order of the elements), I made changed the return type of the method to 'void'.
  • Added error handling in case fieldName is invalid
Vinit_KumarVinit_Kumar

Thanks for sharing :)

Mr.BrooksMr.Brooks
How would I actually implement you code within  my class that i have here? I put the code in and saved it...I am just not sure on how to call it and where to call it. Thanks: 
public List<Note> n1 {get;set;}
    public List<Tuple> combinations{get;set;}
    public string ecid{get;set;}
    public string dcid{get;set;}
    public Datetime CreatedDate;
    String strid = System.currentPagereference().getParameters().get('id');
    public sObject Tuppy;   
    public List<Tuple> pageCombinations {get;set;}
    private Integer pageNumber;
    private Integer PageSize;
    private Integer totalPageNumber;
   
   
   
    public Integer getPageNumber(){
       return pageNumber;
    }
    public Integer getPageSize(){
       return pageSize;
    }
    public Boolean getPreviousButtonEnabled(){
           return !(pageNumber> 1);
   
    }
   
    public Integer getTotalPageNumber(){
       if(totalPageNumber == 0 && combinations != null){

        totalPageNumber = combinations.size() / pageSize;
        Integer mod = combinations.size() - (totalPageNumber * pageSize);
        if(mod > 0)
        totalPageNumber++;
        }
    return totalPageNumber;
    }
   
  
   
    public NotesAttachment(ApexPages.StandardController stdController)
     {
        
       //rlucas Paging
       pageNumber = 0;
       totalPageNumber = 0;
       pageSize = 6;
       ViewData();  

    }
   
    //rlucas view note/attachment
    public PageReference ViewData()
    {
        combinations = null;
        totalPageNumber = 0;
        BindData(1);
        return null;
    }
    private void BindData(Integer newPageIndex){
    try{
        //if(combinations == null)
        combinations = new List<Tuple>();
  
        Tuple tr;
        List<User> usr = [SELECT id, alias, name FROM User WHERE id = :UserInfo.getUserId()];
        List<Attachment> atts =[SELECT Id, Name, createdbyid, createdDate, lastmodifieddate, ContentType, BodyLength, parentID FROM Attachment where parentid=:ApexPages.currentPage().getParameters().get('Id') order by createddate desc];
        List<Note> nt = [select Body, CreatedById, createdDate,Id, Title, parentID, LastModifieddate from Note where parentid=:ApexPages.currentPage().getParameters().get('Id') order by createdDate desc];
        for(Attachment att : atts){
           
                //Instantiating the wrapper sObject
                tr = new Tuple();
               
                //Assigning the wrapper variables
                tr.title = att.Name;
                tr.parentID = att.parentID;
                tr.CreatedById = usr[0].id;
                tr.body = att.ContentType;
                tr.id = att.id;
                tr.usrName = usr[0].Name;
                tr.usrId = usr[0].id;
                tr.LastModifieddate = att.LastModifieddate;
           
                //*Add everything to the List then and there*/
                combinations.add(tr);
            
        } 
         
      
        for(Note con : nt){
            tr = new Tuple();
            tr.title = con.title;
            tr.parentID = con.parentID;
            tr.CreatedById = usr[0].Id;
            tr.body = con.Body;
            tr.id = con.id;
            tr.usrName = usr[0].Name;
            tr.usrId = usr[0].id;
            tr.LastModifieddate = con.LastModifieddate;
           
            //Add the TableRow to the List then and there
            combinations.add(tr);
       }
  
       pageCombinations = new List<Tuple>();
       Transient Integer counter = 0;
       Transient Integer min = 0;
       Transient Integer max = 0;
       if(newPageIndex > pageNumber){
            min = pageNumber * pageSize;
            max = newPageIndex * pageSize;
        } else {
           max = newPageIndex * pageSize;
           min = max - pageSize;
        }
        for(Tuple t : combinations){
           counter ++;
           if(counter > min && counter <=max)
           pageCombinations.add(t);
           //pageCombinations.sort(t);
           compareTime(CreatedDate);
        }
      
        pageNumber = newPageIndex;
        if(pageCombinations == null || pageCombinations.size() <= 0)
        ApexPages.addmessage(new ApexPages.message(ApexPages.severity.INFO,'Data not available for this view.'));
    }
    catch(Exception ex)
    {
        ApexPages.addmessage(new ApexPages.message(ApexPages.severity.FATAL,ex.getMessage()));
    }
    }
   
    public Boolean getNextButtonDisabled(){
       if(combinations == null) return true;
       else
       return((pageNumber * pageSize) >= combinations.size());

    }
    public PageReference nextBtnClick(){
        BindData(pageNumber + 1);
    return null;
    }
    public PageReference previousBtnClick(){
        BindData(pageNumber - 1);
    return null;

    }
   
   
   
    //wrapper class
    global class Tuple
   {
     
      public String id {get; set;}
      public String usrId {get; set;}
      public String usrName {get; set;}
      public String body  {get; set;}
      public String Title {get; set;}
      public String parentID {get; set;}
      public DateTime LastModifieddate{get; set;}
      public String createdbyid  {get; set;}
      public DateTime CreatedDate {get; set;}
      public String Name {get; set;}
      public String ContentType {get; set;}

    }
   
    public NotesAttachment(){
        //combinations = new List<Tuple>();
      
       
    }
   
   /*
    global Integer compareTo(Object compareTo) {
                                        
            Tuple compareToNA = (Tuple)compareTo;
           
      if (NotesAttachment.sortBy.equals('CreatedDateAsc')) {
        if (Tuple.CreatedDate > compareToNA.Tuple.CreatedDate) return 0;
        if (tuple.CreatedDate.daysBetween(compareToNA.Tuple.CreatedDate) > 0) return 1;
        return -1;
        }
   
      }*/
     
    private static Integer compareFields(sObject a, sObject b, Schema.sObjectField field, Schema.DisplayType type) {

        if (type == Schema.DisplayType.Email || type == Schema.DisplayType.Id || type == Schema.DisplayType.Phone ||type == Schema.DisplayType.Picklist ||type == Schema.DisplayType.Reference ||type == Schema.DisplayType.String ||type == Schema.DisplayType.URL) {

            // compareTo method does the same thing as the compare methods below for Numbers and Time

            // compareTo method on Strings is case-sensitive. Use following line for case-sensitivity      
            // return String.valueOf(a.get(field)).compareTo(String.valueOf(b.get(field)));               
            return String.valueOf(a.get(field)).toLowerCase().compareTo(String.valueOf(b.get(field)).toLowerCase());

        }
        else if (type == Schema.DisplayType.Currency ||type == Schema.DisplayType.Double ||type == Schema.DisplayType.Integer ||type == Schema.DisplayType.Percent) {

            return compareNumbers(Double.valueOf(a.get(field)), Double.valueOf(b.get(field)));

        }
        else if (type == Schema.DisplayType.Date ||type == Schema.DisplayType.DateTime ||type == Schema.DisplayType.Time) {

            return compareTime(Datetime.valueOf(a.get(field)), Datetime.valueOf(b.get(field)));

        }
        else {
               return null;
            //throw new SortingException('Type not sortable: ' + type);

        }

    }

    private static Integer compareNumbers(Double a, Double b) {
   
        if  (a < b) { return -1; }
   
        else if (a > b) { return  1; }
   
        else        { return  0; }
   
    }
   
    private static Integer compareTime(Datetime a, Datetime b) {
   
        if  (a < b) { return -1; }
   
        else if (a > b) { return  1; }
   
        else        { return  0; }
   
    }
Mr.BrooksMr.Brooks
I am trying to sort by createdDate