Last active
October 1, 2024 17:55
-
-
Save boxfoot/4166342 to your computer and use it in GitHub Desktop.
Handle cases where one dependent option can be used for multiple controlling options
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Apex doesn't expose dependent picklist info directly, but it's possible to expose. | |
* Approach: | |
* * Schema.PicklistEntry doesn't expose validFor tokens, but they are there, and can be accessed by serializing to JSON | |
* (and then for convenience, deserializing back into an Apex POJO) | |
* * validFor tokens are converted from base64 representations (e.g. gAAA) to binary (100000000000000000000) | |
* each character corresponds to 6 bits, determined by normal base64 encoding rules. | |
* * The binary bits correspond to controlling values that are active - e.g. in the example above, this dependent option | |
* is available for the first controlling field only. | |
* | |
* by Benj Kamm, 2017 | |
* CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/us/) | |
*/ | |
public class HL_FieldDescribeUtil { | |
public static Map<String, List<String>> getDependentOptionsImpl(Schema.SObjectField theField, Schema.SObjectField ctrlField) { | |
// validFor property cannot be accessed via a method or a property, | |
// so we need to serialize the PicklistEntry object and then deserialize into a wrapper. | |
List<Schema.PicklistEntry> contrEntries = ctrlField.getDescribe().getPicklistValues(); | |
List<PicklistEntryWrapper> depEntries = | |
HL_FieldDescribeUtil.wrapPicklistEntries(theField.getDescribe().getPicklistValues()); | |
// Set up the return container - Map<ControllingValue, List<DependentValues>> | |
Map<String, List<String>> objResults = new Map<String, List<String>>(); | |
List<String> controllingValues = new List<String>(); | |
for (Schema.PicklistEntry ple : contrEntries) { | |
String label = ple.getLabel(); | |
objResults.put(label, new List<String>()); | |
controllingValues.add(label); | |
} | |
for (PicklistEntryWrapper plew : depEntries) { | |
String label = plew.label; | |
String validForBits = base64ToBits(plew.validFor); | |
for (Integer i = 0; i < validForBits.length(); i++) { | |
// For each bit, in order: if it's a 1, add this label to the dependent list for the corresponding controlling value | |
String bit = validForBits.mid(i, 1); | |
if (bit == '1') { | |
objResults.get(controllingValues.get(i)).add(label); | |
} | |
} | |
} | |
return objResults; | |
} | |
// Convert decimal to binary representation (alas, Apex has no native method :-( | |
// eg. 4 => '100', 19 => '10011', etc. | |
// Method: Divide by 2 repeatedly until 0. At each step note the remainder (0 or 1). | |
// These, in reverse order, are the binary. | |
public static String decimalToBinary(Integer val) { | |
String bits = ''; | |
while (val > 0) { | |
Integer remainder = Math.mod(val, 2); | |
val = Integer.valueOf(Math.floor(val / 2)); | |
bits = String.valueOf(remainder) + bits; | |
} | |
return bits; | |
} | |
// Convert a base64 token into a binary/bits representation | |
// e.g. 'gAAA' => '100000000000000000000' | |
public static String base64ToBits(String validFor) { | |
if (String.isEmpty(validFor)) return ''; | |
String validForBits = ''; | |
for (Integer i = 0; i < validFor.length(); i++) { | |
String thisChar = validFor.mid(i, 1); | |
Integer val = base64Chars.indexOf(thisChar); | |
String bits = decimalToBinary(val).leftPad(6, '0'); | |
validForBits += bits; | |
} | |
return validForBits; | |
} | |
private static final String base64Chars = '' + | |
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + | |
'abcdefghijklmnopqrstuvwxyz' + | |
'0123456789+/'; | |
private static List<PicklistEntryWrapper> wrapPicklistEntries(List<Schema.PicklistEntry> PLEs) { | |
return (List<PicklistEntryWrapper>) | |
JSON.deserialize(JSON.serialize(PLEs), List<PicklistEntryWrapper>.class); | |
} | |
public class PicklistEntryWrapper { | |
public String active {get; set;} | |
public String defaultValue {get; set;} | |
public String label {get; set;} | |
public String value {get; set;} | |
public String validFor {get; set;} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* getDependentPicklistOptions | |
* by Benj Kamm, 2012 | |
* (inspired by http://iwritecrappycode.wordpress.com/2012/02/23/dependent-picklists-in-salesforce-without-metadata-api-or-visualforce/) | |
* CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/us/) | |
* | |
* Build an Object in which keys are valid options for the controlling field | |
* and values are lists of valid options for the dependent field. | |
* | |
* Method: dependent PickListEntry.validFor provides a base64 encoded | |
* string. After decoding, each of the bits (reading L to R) | |
* corresponds to the picklist values for the controlling field. | |
*/ | |
function getDependentOptions (objName, ctrlFieldName, depFieldName) { | |
// Isolate the Describe info for the relevant fields | |
var objDesc = sforce.connection.describeSObject(objName); | |
var ctrlFieldDesc, depFieldDesc; | |
var found = 0; | |
for (var i=0; i<objDesc.fields.length; i++) { | |
var f = objDesc.fields[i]; | |
if (f.name == ctrlFieldName) { | |
ctrlFieldDesc = f; | |
found++; | |
} else if (f.name == depFieldName) { | |
depFieldDesc = f; | |
found++; | |
} | |
if (found==2) break; | |
} | |
// Set up return object | |
var dependentOptions = {}; | |
var ctrlValues = ctrlFieldDesc.picklistValues; | |
for (var i=0; i<ctrlValues.length; i++) { | |
dependentOptions[ctrlValues[i].label] = []; | |
} | |
var base64 = new sforce.Base64Binary(""); | |
function testBit (validFor, pos) { | |
var byteToCheck = Math.floor(pos/8); | |
var bit = 7 - (pos % 8); | |
return ((Math.pow(2, bit) & validFor.charCodeAt(byteToCheck)) >> bit) == 1; | |
} | |
// For each dependent value, check whether it is valid for each controlling value | |
var depValues = depFieldDesc.picklistValues; | |
for (var i=0; i<depValues.length; i++) { | |
var thisOption = depValues[i]; | |
var validForDec = base64.decode(thisOption.validFor); | |
for (var ctrlValue=0; ctrlValue<ctrlValues.length; ctrlValue++) { | |
if (testBit(validForDec, ctrlValue)) { | |
dependentOptions[ctrlValues[ctrlValue].label].push(thisOption.label); | |
} | |
} | |
} | |
return dependentOptions; | |
} | |
var OBJ_NAME = 'Custom_Object__c'; | |
var CTRL_FIELD_NAME = "Controlling_Field__c"; | |
var DEP_FIELD_NAME = "Dependent_Field__c"; | |
var options = getDependentOptions(OBJ_NAME, CTRL_FIELD_NAME, DEP_FIELD_NAME); | |
console.debug(options); | |
This is a great solution and it helped me a lot. Thank you
For JavaScript, this may be helpful:
Helper functions:
/*
Dependent picklists: https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_describesobjects_describesobjectresult.htm
Base64 characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
A is index 0 so : 000000
I is index 8 so : 001000
Q is index 16 so: 010000
o is index 40 so: 101000
QAAA is 010000 000000 000000 000000
Since binary 1 at 2nd position, not index, means the dependent field is valid option for the 2nd option in parent picklist
IAAA is 001000 000000 000000 000000
Since binary 1 at 3rd position, not index, means the dependent field is valid option for the 3rd option in parent picklist
oAAA is 101000 000000 000000 000000
Since binary 1 at 1st and 3rd position, not index, means the dependent field is valid option for the 1st and 3rd option in parent picklist
Given: 'QAAA', return string like: '010000000000000000000000'
*/
base64EncodingToBinaryBits: (value = '') => {
const base64CharacterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const characterValues = [...value]; // 'QAAA' becomes ['Q', 'A', 'A', 'A']
const binaryValuesOfCharacterPosition = characterValues.map(character => {
const characterPosition = base64CharacterSet.indexOf(character);
const binaryRadix = 2;
const binaryRepresentation = characterPosition.toString(binaryRadix);
const base64Radix = 6; // 2^6 is 64, 6 is the target length of of string
const base64BinaryRepresentation = binaryRepresentation.padStart(base64Radix, '0'); // adds any needed leading 0's
return base64BinaryRepresentation;
});
const binaryBits = binaryValuesOfCharacterPosition.join('');
return binaryBits;
},
/*
Binary strings are interpreted from left to right
Consider on when the binary value is 1
*/
isBinaryValueOnAtIndex: (binaryStringValue = '', index = 0) => {
const bit = binaryStringValue[index];
const isOn = bit
? bit === '1'
: false;
return isOn;
}
Tests for helpers:
describe('Base64 string decoded to binary string', () => {
it('No position on', () => {
const base64PositionOn = undefined;
const binaryPositionOn = '';
const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);
expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
});
it('2nd position on', () => {
const base64PositionOn = 'QAAA';
const binaryPositionOn = '010000000000000000000000';
const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);
expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
});
it('1st and 3rd position on', () => {
const base64PositionOn = 'oAAA';
const binaryPositionOn = '101000000000000000000000';
const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);
expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
});
});
describe('Binary string values on or off', () => {
it('Value on at index for no binary value, index out of bounds', () => {
const binaryValue = '';
const someRandomOutOfBoundsIndex = 5;
const isIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, someRandomOutOfBoundsIndex);
expect(isIndexOn).toEqual(false);
});
it('Value on at index', () => {
const binaryValue = '101000000000000000000000';
const isFirstIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 0);
const isThirdIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 2);
expect(isFirstIndexOn).toEqual(true);
expect(isThirdIndexOn).toEqual(true);
});
it('Value off at index', () => {
const binaryValue = '101000000000000000000000';
const isSecondIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 1);
const isFourthIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 3);
expect(isSecondIndexOn).toEqual(false);
expect(isFourthIndexOn).toEqual(false);
});
});
Hi @fahey252 I want to tweak your code. In my version I assume method may be used several time, so I added memorization. Another tune, is to use integers as bitwise arrays, since bitwise operation are very fast.
const base64CharacterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const memory = new Map();
function base64EncodingToBinaryBits(value) {
if (memory.has(value)) return memory.get(value);
var v = value
.split('')
.map(character => base64CharacterSet
.indexOf(character)
.toString(2)
.padStart(6, '0')
.split('')
.reverse()
.join('')
)
.reverse()
.join('');
v = parseInt(v, 2);
memory.set(value, v);
return v;
}
function isValidFor(validFor, i) {
return base64EncodingToBinaryBits(validFor) & 1 << i;
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In this stackexchange answer, you mentioned it would be
rearranged as bytes: 10000000 00000000 00000000
but in your comments in this gist, it'se.g. 'gAAA' => '100000000000000000000'
.Splitting that in 8ths like the SE answer, that's
10000000 00000000 00000
- is that missing 3 zeros at the end there? Trying to make sure I understand the logic correctly!