Create a Ranking/Sorting Framework for a list of sObjects

November 7, 2012 Appirio
by Kerensa E. Jablonka, Appirio

Do you want data presented in a certain sequence that cannot be solved by sorting the results of a SOQL query?  If a customer searches a catalog for ‘DOOM’, would you want to be able to return a list of DVDs, books, games that matched ‘DOOM’, plus other products that are similar, based on the customer’s preferences? Now you can, by using a ranking framework!  

There are three steps involved in this ranking framework implementation:

  1. Generate a list (List ) of the objects that you want ranked. This list can contain any type of sObject, and does not have to be limited to one type. You can combine Contacts, Accounts, Leads, and custom objects in one list.
  2. Loop through the list and assign a rank (a higher negative value for the rank will result in the object being pushed to the front of the list; a high positive value pushes it to the bottom), based on your ranking criteria. Store the rank result and object to a map that uses the rank as the key, and a list of objects as the value.
  3. Retrieve the keyset from the map (the keyset contains all of the different rank values). Create a list from the set, and then order the list (hence the negative values, as there is no sort descending on the list object). Create a new list of sObjects to store the ranked result. Populate the list by grabbing the data from the map in order of rank.

So, what does this look like in Apex?


STEP 1: GENERATE A LIST OF SOBJECTS

Generate a list of sObjects however you want.  For example, you could run a SOQL query getting specific data.  Or run multiple SOQL queries and then combine the results in one list.  In this example, this list is generated manually, adding some contacts and accounts with values designed to see how the ranking framework does its magic.


public static Integer RANK_HIGH = -10;
public static Integer RANK_LOW = -5;
public static Integer RANK_MEDIUM = -8;
public static String STRING_HIGH = ‘HIGH’;
public static String STRING_MEDIUM = ‘MEDIUM’;
public static String STRING_LOW = ‘LOW’;
public static Integer INTEGER_HIGH = 1000;
public static Integer INTEGER_MEDIUM = 100;
public static Integer INTEGER_LOW = 10;

  public static List createList() {
      List objList = new List();
      String NONAME = ‘No Name’;

      objList = addContactToList(objList, STRING_LOW, STRING_HIGH);
      objList = addContactToList(objList, STRING_LOW, STRING_MEDIUM);
      objList = addContactToList(objList, STRING_LOW, STRING_LOW);

      objList = addAccountToList(objList, STRING_LOW, INTEGER_HIGH);
      objList = addAccountToList(objList, STRING_LOW, INTEGER_MEDIUM);

      objList = addContactToList(objList, STRING_MEDIUM, STRING_HIGH);
      objList = addContactToList(objList, STRING_MEDIUM, STRING_MEDIUM);
      objList = addContactToList(objList, STRING_MEDIUM, STRING_LOW);

      objList = addAccountToList(objList, STRING_HIGH, INTEGER_HIGH);
      objList = addAccountToList(objList, STRING_HIGH, INTEGER_MEDIUM);
      objList = addAccountToList(objList, STRING_HIGH, INTEGER_LOW);
      objList = addAccountToList(objList, STRING_HIGH, 0);
STEP 2: RANK THE LIST
Once you have the list of sObjects, come up with the ranking criteria, which will probably be specific to each object type.  In addition to values in the object itself, rank can be assigned based on other co
nditions, such as user preferences, the day of the week, what specials are currently running for the site, etc.
In order to store the object rank, as well as the reasons why the object was ranked (this is optional, and helpful only if there is an end user evaluating the final ranked list), use an intermediary object called ObjectRank.

  private class ObjectRank {
      Integer rankValue {get; set;}
      Sobject rankedObject {get; set;}
      String rankReasons {get; set;}
    }

Now create methods that assign an ObjectRank for the specific sObject that is being examined.  You’ll probably create a method for each sObject type.  (Note:  the examples contained herein are static and cannot use instance variables; your use case would probably involve a non-static method so that you could have access to data outside of just the sObject itself.)
You can increment/decrement rank based on multiple criteria for that specific object.  Make the final rankValue the cumulative result of the individual rank considerations.

When assigning the numeric rankValue, remember that the list will be sorted in ascending order by rankValue.  Therefore, a high negative value for the rank will result in the object being pushed to the front/top of the list and a high positive value pushes the object to the end/bottom of the list.

In the example below, the Contact is being ranked on the quality of its first and last name (not a great use case, I’m aware, but demonstrative for this purpose).

  public static ObjectRank assignContactRank(Contact myContact) {
      ObjectRank objRank = new ObjectRank();
      Integer rank = 0;
      String rankReason = ”;
      String compareValueString = null;
      String matchString = null;
      if (myContact == null) {
        return null;
      }

      compareValueString = myContact.FirstName;
      matchString = ‘ First Name ‘;
      if (compareValueString.equalsIgnoreCase(STRING_HIGH)) {
          rank = rank + RANK_HIGH;
          rankReason = rankReason + matchString + STRING_HIGH;
      } else if (compareValueString.equalsIgnoreCase(STRING_MEDIUM)) {
          rank = rank + RANK_MEDIUM;
          rankReason = rankReason + matchString + STRING_MEDIUM;
      } else if (compareValueString.equalsIgnoreCase(STRING_LOW)) {
          rank = rank + RANK_LOW;
          rankReason = rankReason + matchString + STRING_LOW;
      } else {
            rank = rank + (-1 * RANK_HIGH);
            rankReason = rankReason + ‘Bad Fit’;
            // this pushes this down toward the bottom of the list;
            // another option is to not change the rank value;
      }

      compareValueString = myContact.LastName;
      matchString = ‘ Last Name ‘;
      if (compareValueString.equalsIgnoreCase(STRING_HIGH)) {
          rank = rank + RANK_HIGH;
          rankReason = rankReason + matchString + STRING_HIGH;
      } else if (compareValueString.equalsIgnoreCase(STRING_MEDIUM)) {
          rank = rank + RANK_MEDIUM;
          rankReason = rankReason + matchString + STRING_MEDIUM;
      } else if (compareValueString.equalsIgnoreCase(STRING_LOW)) {
          rank = rank + RANK_LOW;
          rankReason = rankReason + matchString + STRING_LOW;
      } else {
            rank = rank + (-1 * RANK_HIGH);
            rankReason = rankReason + ‘Bad Fit’;
            // this pushes this down toward the bottom of the list;
            // another option is to not change the rank value;
      }

      objRank.rankedObject = myContact;
      objRank.rankValue = rank;
      objRank.rankReasons = rankReason;
      return objRank;
  }

There is a similar ranking structure for Accounts, ranking on Account Name and the Number of Employees for the Account.

public static ObjectRank assignAccountRank(Account acct) {

      ObjectRank objRank = new ObjectRank();
      Integer rank = 0;
      String rankReason = ”;
      String compareValueString = null;
      Integer compareValueInt = null;
      String matchString = null;
      if (acct == null) {
        return null;
      }
      compareValueInt = acct.NumberofEmployees;
      matchString = ‘ Number of Employees ‘;

      if (compareValueInt >= INTEGER_HIGH) {
          rank = rank + RANK_HIGH;
          rankReason = rankReason + matchString + String.valueof(compareValueInt);
      } else if (compareValueInt >= INTEGER_MEDIUM) {
          rank = rank + RANK_MEDIUM;
          rankReason = rankReason + matchString + String.valueof(compareValueInt);
      } else if (compareValueInt >= INTEGER_LOW) {
          rank = rank + RANK_LOW;
          rankReason = rankReason + matchString + String.valueof(compareValueInt);
      } else {
            rankReason = rankReason + ‘Few Employees’;
      }

      compareValueString = acct.Name;
      matchString = ‘ Account Name ‘;
      if (compareValueString.equalsIgnoreCase(STRING_HIGH)) {
          rank = rank + RANK_HIGH;
          rankReason = rankReason + matchString + STRING_HIGH;
      } else if (compareValueString.equalsIgnoreCase(STRING_MEDIUM)) {
          rank = rank + RANK_MEDIUM;
          rankReason = rankReason + matchString + STRING_MEDIUM;
      } else if (compareValueString.equalsIgnoreCase(STRING_LOW)) {
          rank = rank + RANK_LOW;
          rankReason = rankReason + matchString + STRING_LOW;
      } else {
            rank = rank + (-1 * RANK_HIGH);
            rankReason = rankReason + ‘Bad Fit’;
            // this pushes this down toward the bottom of the list;
            // another option is to not change the rank value;
      }

      objRank.rankedObject = acct;
      objRank.rankValue = rank;
      objRank.rankReasons = rankReason;
      return objRank;
  }

Here’s the generic method for assigning the rank. It delegates the logic to the object-type specific methods (shown above):

public static ObjectRank assignRank(Sobject obj) {

    ObjectRank objRank = null;
    if (obj instanceOf Contact) {
      objRank = assignContactRank((Contact) obj);
    } else if (obj instanceOf Account) {
        objRank = assignAccountRank((Account) obj);
    }

    return objRank;

  }

Now that t
he various sObjects can be ranked, go through the initial list and rank them, storing the results in a map.

The buildRankMap method creates the map of the rank value and the list of sObjects that share that rank.

public static Map> buildRankMap(Map> rankMap, ObjectRank objRank) {

    List objList = null;
      if (rankMap == null) {
          rankMap = new Map>();
      }

      if (rankMap.containsKey(objRank.rankValue)) {
          objList = rankMap.get(objRank.rankValue);
          objList.add(objRank);
      } else {
            objList = new List();
            objList.add(objRank);
            rankMap.put(objRank.rankValue, objList);
      }
     return rankMap;
  }

STEP 3: GET THE RANKED LIST FROM THE MAP


Finally, the getRankedList(Map> rankMap) method sorts the values that are in the map keySet and returns the results in ranked order:


public static List getRankedList(Map> rankMap) {
      List rankedList = new List();

      List rankValueList = new List();
      Set rankValueSet = rankMap.keySet();
      rankValueList.addAll(rankValueSet);
      rankValueList.sort();

      for (Integer rank : rankValueList) {
          rankedList.addall(rankMap.get(rank));
      }
      return rankedList;
  }

You can either use the entire ObjectRank list returned by this method (which gets you the rank value and reasons), or you can get just the list of objects in rank order without the additional detail:
public static List getRankedObjectsFromList(List rankedList) {

      List objList = new List();
      for (ObjectRank objRank : rankedList) {
          objList.add(objRank.rankedObject);
      }
      return objList;
  }

PUTTING IT ALL TOGETHER
The code is really simple to test (once you have generated your list and all your ranking criteria/methods (that’s the hard part!). 


public static void testRankedList() {

      List objList = createList();
      printList(objList);
      ObjectRank objRank = null;

      Map> rankMap = new Map>();

      for (Sobject obj : objList) {
        objRank = assignRank(obj);
        if (objRank != null)   {
            rankMap = buildRankMap (rankMap, objRank);
        }
      }
      List objRankList = getRankedList(rankMap);
      printList(objRankList);
      objList = getRankedObjectsFromList(objRankList);
      printList(objList);
  }

  public static void printList(List objList) {
      System.debug(‘There are ‘ + objList.size() + ‘ items in the list.’);
      for (Sobject obj: objList) {
          System.debug(String.valueOf(obj));
          //System.debug(String.valueOf(obj.getSobjectType()));
      }
  }

PROGRAM OUTPUT:
The original list:

There are 33 items in the list.

Contact:{FirstName=LOW, LastName=HIGH}

Contact:{FirstName=LOW, LastName=MEDIUM}

Contact:{FirstName=LOW, LastName=LOW}

Account:{Name=LOW, NumberOfEmployees=1000}

Account:{Name=LOW, NumberOfEmployees=100}

Contact:{FirstName=MEDIUM, LastName=HIGH}

Contact:{FirstName=MEDIUM, LastName=MEDIUM}

Contact:{FirstName=MEDIUM, LastName=LOW}

Account:{Name=HIGH, NumberOfEmployees=1000}

Account:{Name=HIGH, NumberOfEmployees=100}

Account:{Name=HIGH, NumberOfEmployees=10}

Account:{Name=HIGH, NumberOfEmployees=0}

Contact:{FirstName=HIGH, LastName=No Name}

Contact:{FirstName=MEDIUM, LastName=No Name}

Contact:{FirstName=LOW, LastName=No Name}

Account:{Name=MEDIUM, NumberOfEmployees=1000}

Account:{Name=MEDIUM, NumberOfEmployees=100}

Account:{Name=MEDIUM, NumberOfEmployees=10}

Account:{Name=MEDIUM, NumberOfEmployees=0}

Contact:{FirstName=No Name, LastName=No Name}

Contact:{FirstName=No Name, LastName=No Name}

Contact:{FirstName=No Name, LastName=No Name}

Account:{Name=LOW, NumberOfEmployees=10}

Account:{Name=LOW, NumberOfEmployees=0}

Contact:{FirstName=HIGH, LastName=HIGH}

Contact:{FirstName=HIGH, LastName=MEDIUM}

Contact:{FirstName=HIGH, LastName=LOW}

Account:{Name=No Name, NumberOfEmployees=1000}

Account:{Name=No Name, NumberOfEmployees=100}

Account:{Name=No Name, NumberOfEmployees=10}

Contact:{FirstName=No Name, LastName=HIGH}

Contact:{FirstName=No Name, LastName=MEDIUM}

Contact:{FirstName=No Name, LastName=LOW}

The list of ObjectRanks:

There are 33 items in the list.

ObjectRank:[rankReasons= Number of Employees 1000 Account Name HIGH, rankValue=-20, rankedObject=Account:{Name=HIGH, NumberOfEmployees=1000}]

ObjectRank:[rankReasons= First Name HIGH Last Name HIGH, rankValue=-20, rankedObject=Contact:{FirstName=HIGH, LastNa
me=HIGH}]

ObjectRank:[rankReasons= First Name MEDIUM Last Name HIGH, rankValue=-18, rankedObject=Contact:{FirstName=MEDIUM, LastName=HIGH}]

ObjectRank:[rankReasons= Number of Employees 100 Account Name HIGH, rankValue=-18, rankedObject=Account:{Name=HIGH, NumberOfEmployees=100}]

ObjectRank:[rankReasons= Number of Employees 1000 Account Name MEDIUM, rankValue=-18, rankedObject=Account:{Name=MEDIUM, NumberOfEmployees=1000}]

ObjectRank:[rankReasons= First Name HIGH Last Name MEDIUM, rankValue=-18, rankedObject=Contact:{FirstName=HIGH, LastName=MEDIUM}]

ObjectRank:[rankReasons= First Name MEDIUM Last Name MEDIUM, rankValue=-16, rankedObject=Contact:{FirstName=MEDIUM, LastName=MEDIUM}]

ObjectRank:[rankReasons= Number of Employees 100 Account Name MEDIUM, rankValue=-16, rankedObject=Account:{Name=MEDIUM, NumberOfEmployees=100}]

ObjectRank:[rankReasons= First Name LOW Last Name HIGH, rankValue=-15, rankedObject=Contact:{FirstName=LOW, LastName=HIGH}]

ObjectRank:[rankReasons= Number of Employees 1000 Account Name LOW, rankValue=-15, rankedObject=Account:{Name=LOW, NumberOfEmployees=1000}]

ObjectRank:[rankReasons= Number of Employees 10 Account Name HIGH, rankValue=-15, rankedObject=Account:{Name=HIGH, NumberOfEmployees=10}]

ObjectRank:[rankReasons= First Name HIGH Last Name LOW, rankValue=-15, rankedObject=Contact:{FirstName=HIGH, LastName=LOW}]

ObjectRank:[rankReasons= First Name LOW Last Name MEDIUM, rankValue=-13, rankedObject=Contact:{FirstName=LOW, LastName=MEDIUM}]

ObjectRank:[rankReasons= Number of Employees 100 Account Name LOW, rankValue=-13, rankedObject=Account:{Name=LOW, NumberOfEmployees=100}]

ObjectRank:[rankReasons= First Name MEDIUM Last Name LOW, rankValue=-13, rankedObject=Contact:{FirstName=MEDIUM, LastName=LOW}]

ObjectRank:[rankReasons= Number of Employees 10 Account Name MEDIUM, rankValue=-13, rankedObject=Account:{Name=MEDIUM, NumberOfEmployees=10}]

ObjectRank:[rankReasons= First Name LOW Last Name LOW, rankValue=-10, rankedObject=Contact:{FirstName=LOW, LastName=LOW}]

ObjectRank:[rankReasons=Few Employees Account Name HIGH, rankValue=-10, rankedObject=Account:{Name=HIGH, NumberOfEmployees=0}]

ObjectRank:[rankReasons= Number of Employees 10 Account Name LOW, rankValue=-10, rankedObject=Account:{Name=LOW, NumberOfEmployees=10}]

ObjectRank:[rankReasons=Few Employees Account Name MEDIUM, rankValue=-8, rankedObject=Account:{Name=MEDIUM, NumberOfEmployees=0}]

ObjectRank:[rankReasons=Few Employees Account Name LOW, rankValue=-5, rankedObject=Account:{Name=LOW, NumberOfEmployees=0}]

ObjectRank:[rankReasons= First Name HIGHBad Fit, rankValue=0, rankedObject=Contact:{FirstName=HIGH, LastName=No Name}]

ObjectRank:[rankReasons= Number of Employees 1000Bad Fit, rankValue=0, rankedObject=Account:{Name=No Name, NumberOfEmployees=1000}]

ObjectRank:[rankReasons=Bad Fit Last Name HIGH, rankValue=0, rankedObject=Contact:{FirstName=No Name, LastName=HIGH}]

ObjectRank:[rankReasons= First Name MEDIUMBad Fit, rankValue=2, rankedObject=Contact:{FirstName=MEDIUM, LastName=No Name}]

ObjectRank:[rankReasons= Number of Employees 100Bad Fit, rankValue=2, rankedObject=Account:{Name=No Name, NumberOfEmployees=100}]

ObjectRank:[rankReasons=Bad Fit Last Name MEDIUM, rankValue=2, rankedObject=Contact:{FirstName=No Name, LastName=MEDIUM}]

ObjectRank:[rankReasons= First Name LOWBad Fit, rankValue=5, rankedObject=Contact:{FirstName=LOW, LastName=No Name}]

ObjectRank:[rankReasons= Number of Employees 10Bad Fit, rankValue=5, rankedObject=Account:{Name=No Name, NumberOfEmployees=10}]

ObjectRank:[rankReasons=Bad Fit Last Name LOW, rankValue=5, rankedObject=Contact:{FirstName=No Name, LastName=LOW}]

ObjectRank:[rankReasons=Bad FitBad Fit, rankValue=20, rankedObject=Contact:{FirstName=No Name, LastName=No Name}]

ObjectRank:[rankReasons=Bad FitBad Fit, rankValue=20, rankedObject=Contact:{FirstName=No Name, LastName=No Name}]

ObjectRank:[rankReasons=Bad FitBad Fit, rankValue=20, rankedObject=Contact:{FirstName=No Name, LastName=No Name}]

The object only ranked list:

There are 33 items in the list.

Account:{Name=HIGH, NumberOfEmployees=1000}

Contact:{FirstName=HIGH, LastName=HIGH}

Contact:{FirstName=MEDIUM, LastName=HIGH}

Account:{Name=HIGH, NumberOfEmployees=100}

Account:{Name=MEDIUM, NumberOfEmployees=1000}

Contact:{FirstName=HIGH, LastName=MEDIUM}

Contact:{FirstName=MEDIUM, LastName=MEDIUM}

Account:{Name=MEDIUM, NumberOfEmployees=100}

Contact:{FirstName=LOW, LastName=HIGH}

Account:{Name=LOW, NumberOfEmployees=1000}

Account:{Name=HIGH, NumberOfEmployees=10}

Contact:{FirstName=HIGH, LastName=LOW}

Contact:{FirstName=LOW, LastName=MEDIUM}

Account:{Name=LOW, NumberOfEmployees=100}

Contact:{FirstName=MEDIUM, LastName=LOW}

Account:{Name=MEDIUM, NumberOfEmployees=10}

Contact:{FirstName=LOW, LastName=LOW}

Account:{Name=HIGH, NumberOfEmployees=0}

Account:{Name=LOW, NumberOfEmployees=10}

Account:{Name=MEDIUM, NumberOfEmployees=0}

Account:{Name=LOW, NumberOfEmployees=0}

Contact:{FirstName=HIGH, LastName=No Name}

Account:{Name=No Name, NumberOfEmployees=1000}

Contact:{FirstName=No Name, LastName=HIGH}

Contact:{FirstName=MEDIUM, LastName=No Name}

Account:{Name=No Name, NumberOfEmployees=100}

Contact:{FirstName=No Name, LastName=MEDIUM}

Contact:{FirstName=LOW, LastName=No Name}

Account:{Name=No Name, NumberOfEmployees=10}

Contact:{FirstName=No Name, LastName=LOW}

Contact:{FirstName=No Name, LastName=No Name}

Contact:{FirstName=No Name, LastName=No Name}

Contact:{FirstName=No Name, LastName=No Name}

CONCLUSION

This is just one solution – I’m sure there are better and more efficient ways of doing this, and I’d love to learn about them from you. Feel free to post your comments on the Blog.

Previous Article
Blogging for Salesforce: Chatter Files – A Better Option for Attaching Files to Records
Blogging for Salesforce: Chatter Files – A Better Option for Attaching Files to Records

By Matt Lamb (@sfdcmatt) Chatter is full of so many features it’s often impossible to keep up with everythi...

Next Article
Salesforce and Decimal Places
Salesforce and Decimal Places

by Glenn Weinstein, Appirio@GlennWeinsteinWe learned recently that Salesforce enforces decimal points speci...