├── .gitignore ├── README.md └── src └── classes ├── SortSobs.cls ├── SortSobs.cls-meta.xml ├── SortSobsTests.cls └── SortSobsTests.cls-meta.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .vim-force.com -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apex-sort-sobs 2 | 3 | Utility class that provides a short-hand method for sorting of Salesforce SObjects by any field. Uses a Quicksort implementation with Dynamic Type Inference. 4 | 5 | ## usage 6 | 7 | Sort on Field: 8 | 9 | ``` java 10 | Account[] accs = [SELECT My_Custom_Field__c FROM Account LIMIT 2000]; 11 | SortSobs.ascending(accs, Account.My_Custom_Field__c); 12 | ``` 13 | 14 | Sort in reverse order (desc) 15 | 16 | ``` java 17 | Account[] accs = [SELECT My_Custom_Field__c FROM Account LIMIT 2000]; 18 | SortSobs.descending(accs, Account.My_Custom_Field__c); 19 | ``` 20 | 21 | Sort on parent field: 22 | 23 | ``` java 24 | List entries = [SELECT Name, Account.Name FROM Contact LIMIT 2000]; 25 | SortSobs.ascending(entries, SObjectField[]{ Contact.Account, Account.Name }); 26 | ``` 27 | 28 | When sorting on parent fields, the last item in the SObjectField array is always the field to sort on. All previous items must be relationship fields! 29 | 30 | ## considerations 31 | 32 | This implemenation is a little slower than a `Comparable`, mostly because of overhead in Type casting and we have to run an intial iteration to capture the values to sort. 33 | 34 | ## todo 35 | 36 | - Optimization around casting/general overhead 37 | - Provide more complete benchmarking 38 | 39 | ## benchmarking 40 | 41 | n=2000 42 | 43 | - built in `List.sort()` method 0.017s 44 | - w/ custom comparable wrapper: 0.377s 45 | - Hardcoded Quicksort: 0.435s 46 | - Dynamic Field Quicksort: 0.692s 47 | - Dynamic Field Quicksort on relationship (2 levels): 0.692s 48 | 49 | 50 | Benchmark code for custom comparable wrapper for reference: 51 | 52 | public class ComparatorTest implements Comparable { 53 | public Account te; 54 | public ComparatorTest(Account te){ 55 | this.te = te; 56 | } 57 | 58 | // Implement the compareTo() method 59 | public Integer compareTo(Object compareTo) { 60 | ComparatorTest compareToEmp = (ComparatorTest)compareTo; 61 | if (this.te.Name == compareToEmp.te.Name) return 0; 62 | if (this.te.Name > compareToEmp.te.Name) return 1; 63 | return -1; 64 | } 65 | } 66 | 67 | ## License 68 | MIT 69 | -------------------------------------------------------------------------------- /src/classes/SortSobs.cls: -------------------------------------------------------------------------------- 1 | /** 2 | Author: Charlie Jonas 3 | (github: @chuckjonas, charlie@callawaycloudconsulting.com) 4 | 5 | Description: 6 | Allows for dynamic sorting of SObject Lists without the need to impement Comparables. 7 | - Uses quicksort 8 | 9 | Documentation: Please see https://github.com/ChuckJonas/apex-sort-sobs for useage information & updates 10 | */ 11 | public class SortSobs{ 12 | 13 | /** 14 | * @description Sort SOBS ascending using a relationship 15 | * @param sobs SObjects to sort 16 | * @param sortFields List of SObjectField to capture sorting field. 17 | The last item will be the value sorted on. 18 | All preceeding items must be relationship fields 19 | */ 20 | public static void ascending(SObject[] sobs, SObjectField[] sortFields){ 21 | sort(sobs, sortFields, false); 22 | } 23 | 24 | 25 | /** 26 | * @description Sort SOBS descending using a relationship 27 | * @param sobs SObjects to sort 28 | * @param sortFields List of SObjectField to capture sorting field. 29 | The last item will be the value sorted on. 30 | All preceeding items must be relationship fields 31 | */ 32 | public static void descending(SObject[] sobs, SObjectField[] sortFields){ 33 | sort(sobs, sortFields, true); 34 | } 35 | 36 | /** 37 | * @description Sort SOBS ascending using a field 38 | * @param sobs SObjects to sort 39 | * @param sortField Field to sort on 40 | */ 41 | public static void ascending(SObject[] sobs, SObjectField sortField){ 42 | sort(sobs, new SObjectField[]{ sortField }, false); 43 | } 44 | 45 | /** 46 | * @description Sort SOBS descending using a field 47 | * @param sobs SObjects to sort 48 | * @param sortField Field to sort on 49 | */ 50 | public static void descending(SObject[] sobs, SObjectField sortField){ 51 | sort(sobs, new SObjectField[]{ sortField }, true); 52 | } 53 | 54 | /** HELPERS **/ 55 | 56 | //extracts values, determines type, runs sort 57 | private static void sort(SObject[] sobs, SObjectField[] sortFields, Boolean reverse){ 58 | SObjectField sortField = sortFields.remove(sortFields.size()-1); 59 | 60 | Object[] values = new Object[]{}; 61 | for(SObject sob : sobs){ 62 | values.add(getValueFromRelationship(sob, sortFields, sortField)); 63 | } 64 | 65 | //infer sort type 66 | BaseSOBQuickSort quickSort; 67 | Schema.SoapType sType = sortField.getDescribe().getSoapType(); 68 | if(sType == Schema.SoapType.Integer || sType == Schema.SoapType.Double || sType == Schema.SoapType.Time){ 69 | quickSort = new DecimalSOBQuicksort(); 70 | }else if(sType == Schema.SoapType.String || sType == Schema.SoapType.Id || sType == Schema.SoapType.Base64binary){ 71 | quickSort = new StringSOBQuicksort(); 72 | }else if(sType == Schema.SoapType.Date || sType == Schema.SoapType.DateTime){ 73 | quickSort = new DatetimeSOBQuicksort(); 74 | }else if(sType == Schema.SoapType.Boolean){ 75 | quickSort = new BooleanSOBQuicksort(); 76 | } 77 | quickSort.sort(values, sobs, reverse); 78 | 79 | } 80 | 81 | //extracts value from sob 82 | private static Object getValueFromRelationship(SObject sob, SObjectField[] relationships, SObjectField sortField){ 83 | if(relationships.size() == 0){ 84 | return sob.get(sortField); 85 | } 86 | 87 | SObject parentSob = sob; 88 | for(Integer i = 0; i < relationships.size(); i++){ 89 | parentSob = parentSob.getSObject(relationships[i]); 90 | if(parentSob == null){ 91 | return null; 92 | } 93 | } 94 | return parentSob.get(sortField); 95 | } 96 | 97 | /** QUICKSORT TYPE IMPLEMENTATIONS **/ 98 | 99 | private class DecimalSOBQuicksort extends BaseSOBQuickSort{ 100 | 101 | private override Integer compareToPivot(Object value, Object pivot){ 102 | Decimal val = (Decimal) value; 103 | Decimal piv = (Decimal) pivot; 104 | if(val == piv){ return 0; } 105 | if(val > piv){ return 1; } 106 | return -1; 107 | } 108 | } 109 | 110 | private class StringSOBQuicksort extends BaseSOBQuickSort{ 111 | 112 | private override Integer compareToPivot(Object value, Object pivot){ 113 | 114 | 115 | String val = (String) value; 116 | String piv = (String) pivot; 117 | if(val == piv){ return 0; } 118 | if(val > piv){ return 1; } 119 | return -1; 120 | } 121 | } 122 | 123 | private class DatetimeSOBQuicksort extends BaseSOBQuickSort{ 124 | 125 | private override Integer compareToPivot(Object value, Object pivot){ 126 | DateTime val = (DateTime) value; 127 | DateTime piv = (DateTime) pivot; 128 | if(val == piv){ return 0; } 129 | if(val > piv){ return 1; } 130 | return -1; 131 | } 132 | } 133 | 134 | private class BooleanSOBQuicksort extends BaseSOBQuickSort{ 135 | 136 | private override Integer compareToPivot(Object value, Object pivot){ 137 | Boolean val = (Boolean) value; 138 | Boolean piv = (Boolean) pivot; 139 | if(val && piv || !val && !piv){ return 0; } 140 | if(val){ return 1; } 141 | return -1; 142 | } 143 | } 144 | 145 | /** QUICKSORT BASE CLASS **/ 146 | private abstract class BaseSOBQuickSort{ 147 | private SObject[] sobs; 148 | private Integer length; 149 | private Object[] values; 150 | private Boolean reverse; 151 | 152 | public void sort(Object[] inputValues, SObject[] sobs, Boolean reverse) { 153 | this.reverse = reverse; 154 | 155 | // check for empty or null array 156 | if (inputValues == null || inputValues.size()==0){ 157 | return; 158 | } 159 | 160 | this.sobs = sobs; 161 | values = inputValues; 162 | 163 | length = values.size(); 164 | quicksort(0, length - 1); 165 | } 166 | 167 | // returns 1 if greater, -1 if less than, 0 if equals 168 | private abstract Integer compareToPivot(Object value, Object pivot); 169 | 170 | private void quicksort(Integer low, Integer high) { 171 | Integer i = low, j = high; 172 | 173 | Object pivot = values[low + (high-low)/2]; 174 | 175 | Integer negator = reverse == true ? -1 : 1; 176 | 177 | // into two array 178 | while (i <= j) { 179 | 180 | // if current item from left array < pivot 181 | while (compareToPivot(values[i], pivot) * negator == -1){ 182 | //get the next item in left array 183 | i++; 184 | } 185 | // if current item from right array > pivot 186 | while (compareToPivot(values[j], pivot) * negator == 1) { 187 | //get the next item in right array 188 | j--; 189 | } 190 | 191 | //if left is larger than pivot and right is smaller, exchange 192 | if (i <= j) { 193 | exchangeObject(i, j); 194 | i++; 195 | j--; 196 | } 197 | } 198 | 199 | // recursion 200 | if (low < j) 201 | quicksort(low, j); 202 | if (i < high) 203 | quicksort(i, high); 204 | } 205 | 206 | private void exchangeObject(Integer i, Integer j) { 207 | Object temp = values[i]; 208 | values[i] = values[j]; 209 | values[j] = temp; 210 | 211 | exchangeSob(i, j); 212 | } 213 | 214 | private void exchangeSob(Integer i, Integer j){ 215 | SObject temp = sobs[i]; 216 | sobs[i] = sobs[j]; 217 | sobs[j] = temp; 218 | } 219 | } 220 | } -------------------------------------------------------------------------------- /src/classes/SortSobs.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 40.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/SortSobsTests.cls: -------------------------------------------------------------------------------- 1 | /** 2 | Author: Charlie Jonas 3 | (github: @chuckjonas, charlie@callawaycloudconsulting.com) 4 | 5 | Description: 6 | Tests for SortSobs.cls 7 | 8 | Documentation: Please see https://github.com/ChuckJonas/apex-sort-sobs for useage information & updates 9 | */ 10 | @isTest 11 | private class SortSobsTests { 12 | 13 | @isTest static void test_string_field() { 14 | Account[] accs = new Account[]{}; 15 | Integer c = 200; 16 | for(Integer i = 0; i < c; i++){ 17 | accs.add(new Account( 18 | Name = generateRandomString(30) 19 | )); 20 | } 21 | 22 | Test.startTest(); 23 | SortSobs.ascending(accs, Account.Name); 24 | Test.stopTest(); 25 | 26 | Account previousAccount = accs.remove(0); 27 | for(Account acc : accs){ 28 | System.assert(previousAccount.Name <= acc.Name); 29 | previousAccount = acc; 30 | } 31 | 32 | } 33 | 34 | 35 | @isTest static void test_string_field_desc() { 36 | Account[] accs = new Account[]{}; 37 | Integer c = 200; 38 | for(Integer i = 0; i < c; i++){ 39 | accs.add(new Account( 40 | Name = generateRandomString(30) 41 | )); 42 | } 43 | 44 | Test.startTest(); 45 | SortSobs.descending(accs, Account.Name); 46 | Test.stopTest(); 47 | 48 | Account previousAccount = accs.remove(0); 49 | for(Account acc : accs){ 50 | System.assert(previousAccount.Name >= acc.Name); 51 | previousAccount = acc; 52 | } 53 | 54 | } 55 | 56 | @isTest static void test_number_field() { 57 | Account[] accs = new Account[]{}; 58 | Integer c = 200; 59 | for(Integer i = 0; i < c; i++){ 60 | accs.add(new Account( 61 | AnnualRevenue = Math.random() * 1000 62 | )); 63 | } 64 | 65 | Test.startTest(); 66 | SortSobs.ascending(accs, Account.AnnualRevenue); 67 | Test.stopTest(); 68 | 69 | Account previousAccount = accs.remove(0); 70 | for(Account acc : accs){ 71 | System.assert(previousAccount.AnnualRevenue <= acc.AnnualRevenue); 72 | previousAccount = acc; 73 | } 74 | 75 | } 76 | 77 | @isTest static void test_number_field_desc() { 78 | Account[] accs = new Account[]{}; 79 | Integer c = 200; 80 | for(Integer i = 0; i < c; i++){ 81 | accs.add(new Account( 82 | AnnualRevenue = Math.random() * 1000 83 | )); 84 | } 85 | 86 | Test.startTest(); 87 | SortSobs.descending(accs, Account.AnnualRevenue); 88 | Test.stopTest(); 89 | 90 | Account previousAccount = accs.remove(0); 91 | for(Account acc : accs){ 92 | System.assert(previousAccount.AnnualRevenue >= acc.AnnualRevenue); 93 | previousAccount = acc; 94 | } 95 | 96 | } 97 | 98 | @isTest static void test_date_field() { 99 | Opportunity[] opps = new Opportunity[]{}; 100 | Integer c = 200; 101 | for(Integer i = 0; i < c; i++){ 102 | opps.add(new Opportunity( 103 | CloseDate = Date.today().addDays((Integer)Math.random() * 1000) 104 | )); 105 | } 106 | 107 | Test.startTest(); 108 | SortSobs.ascending(opps, Opportunity.CloseDate); 109 | Test.stopTest(); 110 | 111 | Opportunity previousOpportunity = opps.remove(0); 112 | for(Opportunity opp : opps){ 113 | System.assert(previousOpportunity.CloseDate <= opp.CloseDate); 114 | previousOpportunity = opp; 115 | } 116 | 117 | } 118 | 119 | @isTest static void test_date_field_desc() { 120 | Opportunity[] opps = new Opportunity[]{}; 121 | Integer c = 200; 122 | for(Integer i = 0; i < c; i++){ 123 | opps.add(new Opportunity( 124 | CloseDate = Date.today().addDays((Integer)Math.random() * 1000) 125 | )); 126 | } 127 | 128 | Test.startTest(); 129 | SortSobs.descending(opps, Opportunity.CloseDate); 130 | Test.stopTest(); 131 | 132 | Opportunity previousOpportunity = opps.remove(0); 133 | for(Opportunity opp : opps){ 134 | System.assert(previousOpportunity.CloseDate >= opp.CloseDate); 135 | previousOpportunity = opp; 136 | } 137 | } 138 | 139 | @isTest static void test_datetime_field() { 140 | Task[] tasks = new Task[]{}; 141 | Integer j = 200; 142 | for(Integer i = 0; i < j; i++){ 143 | tasks.add(new Task( 144 | ReminderDateTime = DateTime.now().addDays((Integer)Math.random() * 1000) 145 | )); 146 | } 147 | 148 | Test.startTest(); 149 | SortSobs.ascending(tasks, Task.ReminderDateTime); 150 | Test.stopTest(); 151 | 152 | Task previousTask = tasks.remove(0); 153 | for(Task t : tasks){ 154 | System.assert(previousTask.ReminderDateTime <= t.ReminderDateTime); 155 | previousTask = t; 156 | } 157 | 158 | } 159 | 160 | @isTest static void test_datetime_field_desc() { 161 | Task[] tasks = new Task[]{}; 162 | Integer j = 200; 163 | for(Integer i = 0; i < j; i++){ 164 | tasks.add(new Task( 165 | ReminderDateTime = DateTime.now().addDays((Integer)Math.random() * 1000) 166 | )); 167 | } 168 | 169 | Test.startTest(); 170 | SortSobs.descending(tasks, Task.ReminderDateTime); 171 | Test.stopTest(); 172 | 173 | Task previousTask = tasks.remove(0); 174 | for(Task t : tasks){ 175 | System.assert(previousTask.ReminderDateTime >= t.ReminderDateTime); 176 | previousTask = t; 177 | } 178 | } 179 | 180 | @isTest static void test_boolean_field() { 181 | Case[] cases = new Case[]{}; 182 | Integer j = 200; 183 | for(Integer i = 0; i < j; i++){ 184 | cases.add(new Case( 185 | IsEscalated = Math.random() < .5 ? true : false 186 | )); 187 | } 188 | 189 | Test.startTest(); 190 | SortSobs.ascending(cases, Case.IsEscalated); 191 | Test.stopTest(); 192 | 193 | Boolean switched = false; 194 | for(Case c : cases){ 195 | //should only encounter falses from here on out 196 | if(c.IsEscalated == true){ 197 | switched = true; 198 | } 199 | System.assert((!switched && !c.IsEscalated) || (switched && c.IsEscalated)); 200 | } 201 | 202 | } 203 | 204 | @isTest static void test_boolean_field_desc() { 205 | Case[] cases = new Case[]{}; 206 | Integer j = 200; 207 | for(Integer i = 0; i < j; i++){ 208 | cases.add(new Case( 209 | IsEscalated = Math.random() < .5 ? true : false 210 | )); 211 | } 212 | 213 | Test.startTest(); 214 | SortSobs.descending(cases, Case.IsEscalated); 215 | Test.stopTest(); 216 | 217 | Boolean switched = false; 218 | for(Case c : cases){ 219 | //should only encounter falses from here on out 220 | if(c.IsEscalated == false){ 221 | switched = true; 222 | } 223 | System.assert((!switched && c.IsEscalated) || (switched && !c.IsEscalated)); 224 | } 225 | } 226 | 227 | @isTest static void test_relationship() { 228 | Account[] accs = new Account[]{}; 229 | for(Integer i = 0; i < 100; i++){ 230 | accs.add(new Account( 231 | Name = generateRandomString(30) 232 | )); 233 | } 234 | 235 | Contact[] contacts = new Contact[]{}; 236 | for(Account acc : accs){ 237 | for(Integer i = 0; i < 10; i++){ 238 | contacts.add(new Contact( 239 | Account = acc 240 | )); 241 | } 242 | } 243 | 244 | Test.startTest(); 245 | SortSobs.ascending(contacts, new SObjectField[]{ Contact.AccountId, Account.Name }); 246 | Test.stopTest(); 247 | 248 | Contact previousContact = contacts.remove(0); 249 | for(Contact c : contacts){ 250 | System.assert(previousContact.Account.Name <= c.Account.Name); 251 | previousContact = c; 252 | } 253 | 254 | } 255 | 256 | @isTest static void test_relationship_desc() { 257 | Account[] accs = new Account[]{}; 258 | for(Integer i = 0; i < 100; i++){ 259 | accs.add(new Account( 260 | Name = generateRandomString(30) 261 | )); 262 | } 263 | 264 | Contact[] contacts = new Contact[]{}; 265 | for(Account acc : accs){ 266 | for(Integer i = 0; i < 10; i++){ 267 | contacts.add(new Contact( 268 | Account = acc 269 | )); 270 | } 271 | } 272 | 273 | Test.startTest(); 274 | SortSobs.descending(contacts, new SObjectField[]{ Contact.AccountId, Account.Name }); 275 | Test.stopTest(); 276 | 277 | Contact previousContact = contacts.remove(0); 278 | for(Contact c : contacts){ 279 | System.assert(previousContact.Account.Name >= c.Account.Name); 280 | previousContact = c; 281 | } 282 | 283 | } 284 | 285 | private static String generateRandomString(Integer len) { 286 | final String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; 287 | String randStr = ''; 288 | while (randStr.length() < len) { 289 | Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length()); 290 | randStr += chars.substring(idx, idx+1); 291 | } 292 | return randStr; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/classes/SortSobsTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 40.0 4 | Active 5 | 6 | --------------------------------------------------------------------------------