├── .prettierignore ├── force-app ├── main │ └── default │ │ └── classes │ │ ├── ListUtils.cls-meta.xml │ │ ├── SortableAccount.cls-meta.xml │ │ ├── AccountRatingComparator.cls-meta.xml │ │ ├── SObjectStringFieldComparator.cls-meta.xml │ │ ├── ListUtils.cls │ │ ├── SObjectStringFieldComparator.cls │ │ ├── SortableAccount.cls │ │ └── AccountRatingComparator.cls └── test │ └── default │ └── classes │ ├── SortableAccountTests.cls-meta.xml │ ├── AccountRatingComparatorTests.cls-meta.xml │ ├── SObjectStringFieldComparatorTests.cls-meta.xml │ ├── SortableAccountTests.cls │ ├── SObjectStringFieldComparatorTests.cls │ └── AccountRatingComparatorTests.cls ├── sfdx-project.json ├── config └── project-scratch-def.json ├── package.json ├── .gitignore └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | .localdevserver 6 | .sfdx 7 | .vscode 8 | coverage/ -------------------------------------------------------------------------------- /force-app/main/default/classes/ListUtils.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SortableAccount.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AccountRatingComparator.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/default/classes/SortableAccountTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SObjectStringFieldComparator.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/default/classes/AccountRatingComparatorTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/test/default/classes/SObjectStringFieldComparatorTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "name": "apex-sorting", 9 | "namespace": "", 10 | "sfdcLoginUrl": "https://login.salesforce.com", 11 | "sourceApiVersion": "52.0" 12 | } 13 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "apex-sorting", 3 | "edition": "Developer", 4 | "features": ["EnableSetPasswordInApi"], 5 | "settings": { 6 | "lightningExperienceSettings": { 7 | "enableS1DesktopEnabled": true 8 | }, 9 | "mobileSettings": { 10 | "enableS1EncryptedStoragePref2": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salesforce-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Salesforce App", 6 | "scripts": { 7 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 8 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"" 9 | }, 10 | "devDependencies": { 11 | "@prettier/plugin-xml": "^1.0.2", 12 | "prettier": "^2.3.2", 13 | "prettier-plugin-apex": "^1.10.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sfdx/ 7 | .localdevserver/ 8 | 9 | # LWC VSCode autocomplete 10 | **/lwc/jsconfig.json 11 | 12 | # LWC Jest coverage reports 13 | coverage/ 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # Eslint cache 26 | .eslintcache 27 | 28 | # MacOS system files 29 | .DS_Store 30 | 31 | # Windows system files 32 | Thumbs.db 33 | ehthumbs.db 34 | [Dd]esktop.ini 35 | $RECYCLE.BIN/ 36 | 37 | # Local environment variables 38 | .env 39 | 40 | # VSCode settings 41 | .vscode 42 | -------------------------------------------------------------------------------- /force-app/test/default/classes/SortableAccountTests.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public class SortableAccountTests { 3 | private final static Account a1 = new Account(ShippingCountry = 'A'); 4 | private final static Account a2 = new Account(ShippingCountry = 'A'); 5 | private final static Account a3 = new Account(ShippingCountry = 'B'); 6 | private final static Account a4 = new Account(ShippingCountry = 'C'); 7 | 8 | @isTest 9 | private static void sort_works() { 10 | List accounts = new List{ a4, a2, a3, a1 }; 11 | 12 | SortableAccount.sort(accounts); 13 | 14 | List expected = new List{ a2, a1, a3, a4 }; 15 | System.assertEquals(accounts, expected); 16 | } 17 | 18 | @isTest 19 | private static void compareTo_fails_when_incompatible_type() { 20 | SortableAccount sa1 = new SortableAccount(a1); 21 | Integer i = 1; 22 | 23 | try { 24 | sa1.compareTo(i); 25 | System.assert(false, 'Expected SortException'); 26 | } catch (SortableAccount.SortException e) { 27 | System.assert(true); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ListUtils.cls: -------------------------------------------------------------------------------- 1 | public class ListUtils { 2 | /** 3 | * Sorts a list of objects using bubble sort algorithm and a comparator 4 | */ 5 | public static void sort(List objects, Comparator comparator) { 6 | Integer n = objects.size(); 7 | for (Integer i = 0; i < n - 1; i++) { 8 | for (Integer j = 0; j < n - i - 1; j++) { 9 | if (comparator.compare(objects[j], objects[j + 1]) > 0) { 10 | Object temp = objects[j]; 11 | objects[j] = objects[j + 1]; 12 | objects[j + 1] = temp; 13 | } 14 | } 15 | } 16 | } 17 | 18 | /** 19 | * Interface that specifies how two objects should be compared for ordering 20 | */ 21 | public interface Comparator { 22 | /** 23 | * Compares two objects 24 | * Returns 0 if objects are equal, 1 first object is 'greater' than the second or 2 otherwise. 25 | */ 26 | Integer compare(Object o1, Object o2); 27 | } 28 | 29 | /** 30 | * Exception thrown when Comparator.compare fails. 31 | * This can happen when comparing different object types. 32 | */ 33 | public class CompareException extends Exception { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /force-app/test/default/classes/SObjectStringFieldComparatorTests.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public class SObjectStringFieldComparatorTests { 3 | private final static Account a1 = new Account(ShippingCountry = 'A'); 4 | private final static Account a2 = new Account(ShippingCountry = 'A'); 5 | private final static Account a3 = new Account(ShippingCountry = 'B'); 6 | private final static Account a4 = new Account(ShippingCountry = 'C'); 7 | 8 | @isTest 9 | private static void sort_works() { 10 | List accounts = new List{ a4, a2, a3, a1 }; 11 | 12 | ListUtils.sort( 13 | accounts, 14 | new SObjectStringFieldComparator('ShippingCountry') 15 | ); 16 | 17 | List expected = new List{ a2, a1, a3, a4 }; 18 | System.assertEquals(accounts, expected); 19 | } 20 | 21 | @isTest 22 | private static void sort_fails_when_incompatible_types() { 23 | String someString; 24 | List objects = new List{ a1, someString }; 25 | 26 | try { 27 | ListUtils.sort( 28 | objects, 29 | new SObjectStringFieldComparator('ShippingCountry') 30 | ); 31 | System.assert(false, 'Expected ListUtils.CompareException'); 32 | } catch (ListUtils.CompareException e) { 33 | System.assert(true); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /force-app/test/default/classes/AccountRatingComparatorTests.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public class AccountRatingComparatorTests { 3 | private final static Account a1 = new Account(Rating = 'Cold'); 4 | private final static Account a2 = new Account(Rating = 'Cold'); 5 | private final static Account a3 = new Account(Rating = 'Hot'); 6 | private final static Account a4 = new Account(Rating = null); 7 | 8 | @isTest 9 | private static void sort_works() { 10 | List accounts = new List{ a4, a2, a3, a1 }; 11 | 12 | ListUtils.sort(accounts, new AccountRatingComparator()); 13 | 14 | List expected = new List{ a3, a1, a2, a4 }; 15 | System.assertEquals(accounts, expected); 16 | } 17 | 18 | @isTest 19 | private static void sort_fails_when_incompatible_types() { 20 | String someString; 21 | List objects = new List{ a1, someString }; 22 | 23 | try { 24 | ListUtils.sort(objects, new AccountRatingComparator()); 25 | System.assert(false, 'Expected ListUtils.CompareException'); 26 | } catch (ListUtils.CompareException e) { 27 | System.assert(true); 28 | } 29 | } 30 | 31 | @isTest 32 | private static void sort_fails_when_invalid_rating_value() { 33 | Account invalidRatingAccont = new Account(Rating = 'Invalid'); 34 | List accounts = new List{ a1, invalidRatingAccont }; 35 | 36 | try { 37 | ListUtils.sort(accounts, new AccountRatingComparator()); 38 | System.assert(false, 'Expected ListUtils.CompareException'); 39 | } catch (ListUtils.CompareException e) { 40 | System.assert(true); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SObjectStringFieldComparator.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Comparator that lets you sort a list of SObject alphabetically based on a String field. 3 | * Example: ListUtils.sort(accounts, new SObjectStringFieldComparator('ShippingCountry')); 4 | * 5 | * Tip: don't use this to sort by Name as SObjects are already sorted by Name by default when calling List.sort() 6 | * https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_list_sorting_sobject.htm 7 | */ 8 | public class SObjectStringFieldComparator implements ListUtils.Comparator { 9 | private final String fieldName; 10 | 11 | public SObjectStringFieldComparator(String fieldName) { 12 | this.fieldName = fieldName; 13 | } 14 | 15 | public Integer compare(Object o1, Object o2) { 16 | // Make sure that the two Object instances are SObject instances 17 | if (!(o1 instanceof SObject && o2 instanceof SObject)) { 18 | throw new ListUtils.CompareException( 19 | 'Objects must both be SObject in order to be compared: ' + 20 | o1 + 21 | ' AND ' + 22 | o2 23 | ); 24 | } 25 | SObject so1 = (SObject) o1; 26 | SObject so2 = (SObject) o2; 27 | 28 | // We assume that the field value is a String 29 | // with that, we can cast values to String in order to compare them 30 | // This cast is required because the Object type cannot be compared with 'greater' operator 31 | String value1 = (String) so1.get(fieldName); 32 | String value2 = (String) so2.get(fieldName); 33 | if (value1 == value2) { 34 | return 0; 35 | } 36 | if (value1 > value2) { 37 | return 1; 38 | } 39 | return -1; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Warning** 2 | > Starting from Winter '24, use the new [Comparator](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_interface_System_Comparator.htm) interface instead of this. 3 | 4 | # Sample Code for Sorting in Apex 5 | 6 | This repository contains sample code for advanced sorting with Apex. It presents two approaches that go beyond the default [List.sort](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_collections_lists_sorting.htm) method. 7 | 8 | ## Sorting Lists of sObjects with Custom Ordering 9 | 10 | [SortableAccount](force-app/main/default/classes/SortableAccount.cls) demonstrates how you can sort a list of Account records based on the `ShippingCountry` field: 11 | 12 | ```apex 13 | SortableAccount.sort(List); 14 | ``` 15 | 16 | ## Sorting Lists with Reusable Comparators 17 | 18 | [ListUtils](force-app/main/default/classes/ListUtils.cls) and the [Comparator](force-app/main/default/classes/ListUtils.cls#L21) interface demonstrate how you can sort lists with custom reusable comparators such as: 19 | 20 | ```apex 21 | // Sort a list of accounts alphabetically based on shipping country 22 | ListUtils.sort(accounts, new SObjectStringFieldComparator('ShippingCountry')); 23 | 24 | // Sort a list of accounts alphabetically based on industry 25 | ListUtils.sort(accounts, new SObjectStringFieldComparator('Industry')); 26 | 27 | // Sort a list of accounts based on rating values 28 | // as defined in the rating picklist order (non-alphabetical sort) 29 | ListUtils.sort(accounts, new AccountRatingComparator()); 30 | ``` 31 | 32 | **A note on performance:** `ListUtils` uses a bubble sort algorithm. This works fine in most cases but other algorithms may be more efficient depending on the type and volume of data that you are sorting. 33 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SortableAccount.cls: -------------------------------------------------------------------------------- 1 | public class SortableAccount implements Comparable { 2 | private final Account account; 3 | 4 | public SortableAccount(Account account) { 5 | this.account = account; 6 | } 7 | 8 | /** 9 | * Sort accounts based on ShippingCountry 10 | */ 11 | public Integer compareTo(Object otherObject) { 12 | // For additional type safety, check if otherObject is a SortableAccount 13 | // if not, throw a SortException 14 | if (!(otherObject instanceof SortableAccount)) { 15 | throw new SortException('Can\'t sort with incompatible type'); 16 | } 17 | // Cast otherObject to SortableAccount and compare it 18 | SortableAccount other = (SortableAccount) otherObject; 19 | if (this.account.ShippingCountry == other.account.ShippingCountry) { 20 | return 0; 21 | } 22 | if (this.account.ShippingCountry > other.account.ShippingCountry) { 23 | return 1; 24 | } 25 | return -1; 26 | } 27 | 28 | /** 29 | * Sorts a list of Account records using SortableAccount 30 | */ 31 | public static void sort(List accounts) { 32 | // Convert List into List 33 | List sortableAccounts = new List(); 34 | for (Account acc : accounts) { 35 | sortableAccounts.add(new SortableAccount(acc)); 36 | } 37 | 38 | // Sort accounts using SortableAccount.compareTo 39 | sortableAccounts.sort(); 40 | 41 | // Overwrite the account list provided in the input parameter 42 | // with the sorted list. Doing this avoids a return statement 43 | // and is less verbose for the method user. 44 | for (Integer i = 0; i < accounts.size(); i++) { 45 | accounts[i] = sortableAccounts[i].account; 46 | } 47 | } 48 | 49 | public class SortException extends Exception { 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AccountRatingComparator.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Compares account records based on Rating value as defined in the picklist order (not alphabetically). 3 | * This is used to sort account lists. 4 | */ 5 | public class AccountRatingComparator implements ListUtils.Comparator { 6 | // Cache for converting rating values from String to Integer 7 | private static Map ratingValues; 8 | 9 | static { 10 | // Build cache of String to Integer rating values 11 | ratingValues = new Map(); 12 | List ratingPicklistEntries = Account.Rating.getDescribe() 13 | .getPicklistValues(); 14 | for (Integer i = 0; i < ratingPicklistEntries.size(); i++) { 15 | ratingValues.put(ratingPicklistEntries[i].getValue(), i); 16 | } 17 | } 18 | 19 | public Integer compare(Object o1, Object o2) { 20 | // Make sure that the two Object instances are Account instances 21 | if (!(o1 instanceof Account && o2 instanceof Account)) { 22 | throw new ListUtils.CompareException( 23 | 'Accounts cannot be compared: ' + 24 | o1 + 25 | ' AND ' + 26 | o2 27 | ); 28 | } 29 | Integer value1 = getRatingValueAsInteger(((Account) o1).Rating); 30 | Integer value2 = getRatingValueAsInteger(((Account) o2).Rating); 31 | if (value1 == value2) { 32 | return 0; 33 | } 34 | if (value1 > value2) { 35 | return 1; 36 | } 37 | return -1; 38 | } 39 | 40 | private Integer getRatingValueAsInteger(String rating) { 41 | // If rating is null, return highest value 42 | if (rating == null) { 43 | return ratingValues.size(); 44 | } 45 | 46 | // Get rating value based on String 47 | Integer value = ratingValues.get(rating); 48 | if (value == null) { 49 | throw new ListUtils.CompareException('Invalid rating value: ' + rating); 50 | } 51 | return value; 52 | } 53 | } 54 | --------------------------------------------------------------------------------