function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
andresperezandresperez 

A gift for you: Sorting a List<sObject>.

Dear salesforce.com users,

 

I want to share with you one Appex class that sorts a List<sObject> by any field in ascending or descending order. The List<sObject> is generated by any SOQL statement, so it can be made of custom and/or standard objects. 

 

Using this class is quite simple, and because I have written unit tests that validates 100% of the code you can easily use it in production sytems.

 

The class performs quite well because the sorting is done in memory (using Maps, Sets and Lists). It also detects if the sort has been done for this field so it does not need to resort (even if it is in reverse order).

 

Before going into details of the Appex class, let me show you how the class is used...

 

The VisualForce page:

Nothing fancy here... Just a page building a datatable with three columns and command buttons on the table headers to sort the data.

<apex:page controller="aaSorterContact">
<apex:form >
<apex:pageBlock >
<apex:pageBlockSection columns="1" ID="AjaxTable">
<apex:datatable value="{!List}" var="acc" Border="1" cellspacing="1" cellpadding="5">
<apex:column >
<apex:facet name="header">
<apex:commandButton action="{!SortByName}"

value="Sort By Name" rerender="AjaxTable" />
</apex:facet>
<apex:outputText value="{!acc.Name}" />
</apex:column>
<apex:column >
<apex:facet name="header">
<apex:commandButton action="{!SortByPhone}"

value="Sort By Phone" rerender="AjaxTable" />
</apex:facet>
<apex:outputText value="{!acc.Phone}" />

</apex:column>
<apex:column >
<apex:facet name="header">

<apex:commandButton action="{!SortByAccount}"

value="Sort By Account" rerender="AjaxTable" />
</apex:facet>
<apex:outputText value="{!acc.Account.Name}" />

</apex:column>
</apex:datatable>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>

The controller:

Couple things going in here, but that is just to make the page look nice... Nothing really to do with the sorting.

public class aaSorterContact {
private String sortedBy = null;
private Boolean sortAscending = null;
private AP_SortHelper sorter = new AP_SortHelper();
private List<Contact> sortedList = null;

public aaSorterContact() {
sorter.originalList = [SELECT Name, Phone, Account.Name FROM Contact];
}
public PageReference SortByName() {
setSortedBy('NAME');
sortedList = (List<Contact>) sorter.getSortedList('Name', sortAscending);
return null;
}
public PageReference SortByAccount() {
setSortedBy('ACCOUNT');
sortedList = (List<Contact>) sorter.getSortedList('Account.Name', sortAscending);
return null;
}
public PageReference SortByPhone() {
setSortedBy('PHONE');
sortedList = (List<Contact>) sorter.getSortedList('Phone', sortAscending);
return null;
}
public List<Contact> getList() {
if (sortedList == null) {
SortByName();
}
return sortedList;
}
private void setSortedBy(String value) {
if (sortedBy == value) {
sortAscending = !sortAscending;
} else {
sortAscending = true;
}
sortedBy = value;
}
}

 

Let me talk about the easy part first...

 

There are methods that answer the calls from the commandbuttons on the page:

 

  • SortByName
  • SortByAccount
  • SortByPhone

 

These methods follow the same structure:

setSortedBy('NAME');
sortedList = (List<Contact>) sorter.getSortedList('Name', sortAscending);
return null;

First, it calls a method setSortedBy() to find out the ascending or descending order. If the user clicks on a different button, the table is sorted ascending by that column, ortherwise the order is inverted from Ascending to descending and viceversa.

 

Second, it calls the method in the Appex class that does the sorting. (I will explain on detail how to use the Appex class, keep reading) :smileywink:

 

Finally, the controller's method returns a null value to the page.

 

The controller's constructor gets the list from the database.

public aaSorterContact() {
sorter.originalList = [SELECT Name, Phone, Account.Name FROM Contact];
}

Since the buttons use the rerendered propery (using AJAX), the class constructor is only called at the initial page load rather than every time the buttons are clicked, therefore the SOQL gets called only once regardless of how many times the data table gets sorted.

 

Finally, the more interesting part...

 

The Appex class that sorts:

You don't really need to understand how this class works to use it, but those of you who are interested...

public class AP_SortHelper {     // <ID, Position>
private Map<String, Integer> listPosition = null; // <FieldName, <FieldValues>>
private Map<String, List<String>> sortedFieldValuesPerFieldName = null; // <FieldName, <FieldValue, <IDs>>>
private Map<String, Map<String, List<String>>> sObjectIDsPerFieldNames = null;

// Properties
public List<sObject> originalList {get; set;}

// Constructor
public AP_SortHelper() {
originalList = null;
}// Public Method
public List<sObject> getSortedList(String fieldName, Boolean ascending) {
if (originalList == null) {
// Assume that originalList has a not NULL value.
// If the class who uses this method has not assigned a value it will get an Exception which
// needs to be handled by the calling class. // Force the exception...
originalList.clear();
} // Make field name uppercase
fieldName = fieldName.toUpperCase(); // Get sorted list
return makeSortedList(fieldName, ascending);
}
public List<sObject> getSortedList(List<sObject> originalList, String fieldName, Boolean ascending) {
this.originalList = originalList;
sortedFieldValuesPerFieldName = null;
return getSortedList(fieldName, ascending);
}

// Private Methods
private void InitializeFieldName(String fieldName) {
String sObjectID;
Integer position;
String fieldValue;
List<String> sObjectIDs = null;
Set<String> valuesForFieldSet = null; // Sets automatically omit duplicate values
List<String> valuesForFieldList = null;
Map<String, List<String>> sObjectIDsPerFieldValues = null;

// Make sortedFieldValuesPerFieldName
if (sortedFieldValuesPerFieldName == null) {
listPosition = new Map<String, Integer>();
sortedFieldValuesPerFieldName = new Map<String, List<String>>();
sObjectIDsPerFieldNames = new Map<String, Map<String, List<String>>>();
}

// Get (or create) map of sObjectIDsPerFieldValues
sObjectIDsPerFieldValues = sObjectIDsPerFieldNames.get(fieldName);
if (sObjectIDsPerFieldValues == null) {
sObjectIDsPerFieldValues = new Map<String, List<String>>();
sObjectIDsPerFieldNames.put(fieldName, sObjectIDsPerFieldValues);
}
if (!sortedFieldValuesPerFieldName.keySet().contains(fieldName)) {
// Objects need to be initialized
position = 0;
valuesForFieldSet = new Set<String>();
listPosition = new Map<String, Integer>();

for (sObject sObj : originalList) {
sObjectID = sObj.ID;
fieldValue = getValue(sObj, fieldName);

// Add position to list
listPosition.put(sObjectID, position++);

// Add the value to the set (sets rather than lists to prevent duplicates)
valuesForFieldSet.add(fieldValue);

// Get (or create) map of sObjectIDs
sObjectIDs = sObjectIDsPerFieldValues.get(fieldValue);
if (sObjectIDs == null) {
sObjectIDs = new List<String>();
sObjectIDsPerFieldValues.put(fieldValue, sObjectIDs);
}

// Add ID to sObjectIDs
sObjectIDs.add(sObjectID);
}

// Sort set items (Need to convert to list)
valuesForFieldList = new List<String>();
valuesForFieldList.addAll(valuesForFieldSet);
valuesForFieldList.sort();

// Now add it to the map.
sortedFieldValuesPerFieldName.put(fieldName, valuesForFieldList);
}
}
private List<sObject> makeSortedList(String fieldName, Boolean ascending) {
Integer position;
List<String> sObjectIDs = null;
List<String> valuesForFieldList = null; // Initialize objects
InitializeFieldName(fieldName); // Get a list of the same type as the "originalList"
List<sObject> outputList = originalList.clone();
outputList.clear(); // Get a list of sorted values
valuesForFieldList = sortedFieldValuesPerFieldName.get(fieldName);

// for each sorted value
for (String fieldValue : valuesForFieldList) {
// Get lisft of IDs
sObjectIDs = sObjectIDsPerFieldNames.get(fieldName).get(fieldValue);

// for each ID
for (String ID : sObjectIDs) {
// Get position in originalList
position = listPosition.get(ID); // Add each sObject to the list.
if ((ascending) || (outputList.size()==0)) {
outputList.add(originalList[position]);
} else {
outputList.add(0, originalList[position]);
}
}
}
return outputList;
}
private static String getValue(sObject sObj, String fieldName) {
// This returns the sObject desired in case the fieldName refers to a linked object.
Integer pieceCount;
String[] fieldNamePieces;

fieldNamePieces = fieldName.split('\\.');
pieceCount = fieldNamePieces.size();
for (Integer i = 0; i < (pieceCount-1); i++) {
sObj = sObj.getSObject(fieldNamePieces[i]);
}
return String.valueOf(sObj.get(fieldNamePieces[pieceCount-1]));
}

// Unit testing
/*
static testMethod void testSortCustomObject() {
List<TPValue__c> TPValues;
AP_SortHelper sorter = new AP_SortHelper();
String fieldName;

TPValues = [SELECT TPName__r.TPName__c, Value__c FROM TPValue__c LIMIT 50];
fieldName = 'Value__c';
testOrderedList(sorter.getSortedList(TPValues, fieldName, true), fieldName, true);

fieldName = 'TPName__r.TPName__c';
testOrderedList(sorter.getSortedList(TPValues, fieldName, true), fieldName, true);
}
*/
static testMethod void testSimpleField_Ascending() {
testSortingContacts('Name', true);
}
static testMethod void testSimpleField_Descending() {
testSortingContacts('Name', False);
}
static testMethod void testLookupField_Ascending() {
testSortingContacts('Account.Name', True);
}
static testMethod void testLookupField_Decending() {
testSortingContacts('Account.Name', False);
}
static testMethod void testMultipleCalls() {
AP_SortHelper sorter;
sorter = testSortingContacts(null, 'Name', true);
testSortingContacts(sorter, 'Name', False);
testSortingContacts(sorter, 'Account.Name', True);
testSortingContacts(sorter, 'Account.Name', False);
}
static testMethod void testForgotOriginalList() {
Boolean exceptionDetected = false;
AP_SortHelper sorter = new AP_SortHelper();
try {
sorter.getSortedList('Name', true);
} catch (NullPointerException e) {
exceptionDetected = true;
}
System.assert(exceptionDetected);
}
static testMethod void testPassingList() {
AP_SortHelper sorter = new AP_SortHelper();
List<Contact> contacts = [SELECT Name, Phone, Account.Name FROM Contact LIMIT 50];
List<Contact> sortedList = (List<Contact>) sorter.getSortedList(contacts, 'Name', true);
testOrderedList(sortedList, 'Name', true);
}
private static void testSortingContacts(string fieldName, Boolean isAscending) {
testSortingContacts(null, fieldName, isAscending);
}
private static AP_SortHelper testSortingContacts(AP_SortHelper sorter, string fieldName, Boolean isAscending) {
// If sorted is null,create it.
if (sorter == null) {
sorter = new AP_SortHelper();
sorter.originalList = [SELECT Name, Phone, Account.Name FROM Contact LIMIT 50];
}

// Sort list
List<Contact> sortedList = (List<Contact>) sorter.getSortedList(fieldName, isAscending); // Test sort order
testOrderedList(sortedList, fieldName, isAscending);

return sorter;
}
private static void testOrderedList(List<sObject> sortedList, string fieldName, Boolean isAscending) {
String lastValue = null;
String currentValue = null; for (sObject sObj : sortedList) {
currentValue = getValue(sObj, fieldName);
if ((lastValue != null) && (currentValue != null)) { String strDebug = '';
strDebug += '\n--------------------------------------------------------------';
strDebug += '\nSTART';
strDebug += '\n--------------------------------------------------------------';
strDebug += '\n[Ascending:'+isAscending+']';
strDebug += '\n[Previous:'+lastValue+'] [IsNull():'+(lastValue==null)+']';
strDebug += '\n[Current:'+currentValue+'] [IsNull():'+(currentValue==null)+']';
strDebug += '\n[CompareTo:'+(currentValue.compareTo(lastValue))+']';
strDebug += '\n--------------------------------------------------------------';
strDebug += '\nEND';
strDebug += '\n--------------------------------------------------------------';
System.debug(strDebug); if (isAscending) {
System.assertEquals(currentValue.compareTo(lastValue)>=0, true);
} else {
System.assertEquals(currentValue.compareTo(lastValue)<=0, true);
}
}
lastValue = currentValue;
}
}
}

 

How to use this class?

  1. Create an instance of this class AP_SortHelper()
  2. Assign the list to sort. Get this list using SOQL.
  3. Call the getSortedList() method which takes two fields:
    1. The name of the field as it was used in the SOQL
    2. The order (true for ascending, false for descending

 


For now, I have one question to the group...

 

This message is getting long... Is there a better place to post it? The way I see it, AppeXchange is applications not for independent utility classes.

Message Edited by andresperez on 01-28-2009 10:45 AM
DianeMDianeM

Thank you for sharing this.  I dropped it into my code and it just worked!!!  I wonder if the Code Share project would be a good place for this and other helpful classes to reside.

 

Diane

cmarz1cmarz1

Thanks for this.  But I have a problem, this worked fine in myt dev envirorment but when I tried to deploy it I get this error message when running the tests:

 

System.NullPointerException: Attempt to de-reference a null object

 

Error on line: 

return String.valueOf(sObj.get(fieldNamePieces[pieceCount-1]));

 

It happens on  

testLookupField_Ascending

testMultipleCalls

testLookupField_Decending

 

Any ideas on why I'm getting this?

Message Edited by cmarz1 on 03-12-2009 07:17 AM
sparktestsparktest
Can this be used on just an Apex class?  .....not a visualforce setup?  I am running into 'to many script statements due to the recrds in a SObject List not being sorted...and having to write loops to 'find' the records needed prior to processing.  When there are potentially 120 or more records the top end processing yields 120*120 cycles....if i got that right....apparently the 10001 limit doesn't like it....
DianeMDianeM
I believe that the 10001 limit is with respect to SOQL queries and the multiple loops should not affect that - so you may have a different problem.  With respect to the sort, it worked very well for me to solve exactly this problem.  I retrieve my list of objects, use this sort to put the entries in the list in the order I need and then continue my processing.  The sort gets invoked in the Visualforce controller.
sparktestsparktest

I am not familiar with visualforce controllers....I am using Apex Triggers and Class Only......would you have any help on how to use it in that way instead......

 

In a nutshell, I am pulling a select group of records from a detail object, running through them all and updating each one's values in sequence based on a value entered on the trigger record.

 

I do a 'running total' and similar calcualtions, so it is important that it goes through them in order.

 

This is the last piece of a big puzzle.  I have another tool to use, but would prefer to have it done this way.

 

Any help would be great...I need to have this figured out today or move on.....

 

Thanks,

 

 

DianeMDianeM

The Visualforce controller is just another Apex class - based on your note I thought you were using A Visualforce page.  So wherever it is that you are reading your objects - use this sort process there.

andresperezandresperez

cmarz1,

 

Were you able to find the problem... I do not think it has to do with the code, but it may have to do with the data in production. Were you able to figure this one out?

 

If not, try putting System.debug() statements before the line to see which object is null...

 

System.debug(pieceCount);   

System.debug(fieldNamePieces);

System.debug(fieldNamePieces[pieceCount-1]);  

System.debug(sObj.get(fieldNamePieces[pieceCount-1])); 

System.debug(String.valueOf(sObj.get(fieldNamePieces[pieceCount-1])));

 

This should help you find the problem.

 

Message Edited by andresperez on 03-29-2009 10:27 AM
sparktestsparktest

For anyone else.......I got a sort working from the following location

 

http://blog.sforce.com/sforce/2008/09/sorting-collect.html

Message Edited by sparktest on 03-29-2009 12:25 PM
lakhan.prajapatilakhan.prajapati
thanks to post the code here , can I use this sorting algorithm to sort a list of sobjects with the sortfield having duplicate values?
aperezSFDCaperezSFDC
Yes... but it will not look sorted and more grouped. It could probably be better to create a formula field appending multiple fields, and using that key to sort.
gazazellogazazello

Thanks!

 

I have a question. How do I sort a list with my custom objects, which are not stored in the database? For instance, I have a class CustomClass1 and List<CustomClass1>. How can I have List<CustomClass1> sorted?

aperezSFDCaperezSFDC

The code was originally built for List<sobject>, so the class is not useful for other lists. You need to build your own class...  :-(

gazazellogazazello

Thank you for a quick reply though! But could you give me some hints for how could I customize your nice sorting class for my custom class if you have time for that?

Reppin__505Reppin__505

You don't see this kind of full fledged solution posted out of the kindness of the developers heart very often. Two years later and this is still relevant.

 

Mad love to . I havn't tried it yet but i will.

 

Thanks again.

Reppin__505Reppin__505

I didn't realize this does not work with Decimals or Integers. I spent two hours trying to get this to sort a "Cost" field right. Could not get it to sort that kind of data correctly.

aperezSFDCaperezSFDC
Hi, I think problem you are having isthat your numbers are being sorted alphabetically, rather than numerically. Basically is the output looking like this 1,12,2,25, Rather than 1,2,12,25 If so, it is because the lists/sets i used for sorting are rather than . If not, please let me know what is not working and i willl give you a hand.
Reppin__505Reppin__505

Yes it is sorting as String type rather than Decimal type. You're helper class is rather complex, and i tried to change some of the lists to type Decimal but it turned out to be like diving into a deep rabbit whole and i ended up not knowing what the heck i was doing. 

 

This class you wrote is a killer solution and is precisely what i need, and if the ability to sort by both types string and decimal is possible, that would be most ideal.

 

Please let me know what i could change or add...thanks so much.

Reppin__505Reppin__505

Do you have any advice on sorting Numberic types?

aperezSFDCaperezSFDC
Hi, I have been trying to get sometime to work on the code, so it handles numeric values, but I have not had the time :-( On the other hand, if you could convert your numbers from something like 1 2 12 15 to string like this: 001 002 012 015 Then you could use this string values for the sorting... It is not the best answer, but it will be the quickest
vikiviki

Account field cant be empty for contacts.That causes the null pointer exception

Double check that !! 

Shane ThomasShane Thomas

Thanks for this!

 

miteshsuramiteshsura

Hi,

You may have already found an solution to this, but I will post it anyways for others to take advantage. I was in the same boat as you were, I was trying to sort a wrapper class.

Here it goes...

Instead of a list<sObject> to order, I have a list<WrapperClass> to sort, which is not much different when it comes to sorting.

In the wrapper class I had custom Object, boolean, and a sort order fields.

What I did was iterate original non-primitieve list I wanted to sort, and copied just the field "sortOrder" into new list<integer>, lets say mySortOrder.

//CODE
list<integer> mySortOrder = new list<integer>();

for(list<non-primitive> y : orginalList){

   mySortOrder.add(y.sortOrder);

}

//Once I had that, I invoked

mySortOrder.Sort();

Than I have two for loops:
Outer for loop --> iterate over mySortOrder
Inner for loop --> iternate over org list I wanted to sort

//CODE
for(integer x : mySortOrder){

  for(list<non-primitive> y : orginalList){

 

  if(x == y.sortOrder){

 

   // Your business logic //

   }

  }

}

Depending on the needs, one may add some validation. Works great for our needs, and very simple. I have not tested on huge set, but works good for 100-200 records in the list. Does anyone see any issue??

Thought someone can take advantage of this.

regards

Mitesh

nwallacenwallace

I wanted to do this same thing, but I found this implementation confusing to follow, so I put one together myself.  It can be found at this link:

 

http://boards.developerforce.com/t5/Apex-Code-Development/Sort-List-of-sObjects-by-Field-Value/td-p/574745

 

Let me know what you think!