diff --git a/flow_action_components/String Normaliser/README.md b/flow_action_components/String Normaliser/README.md new file mode 100644 index 000000000..0854707b0 --- /dev/null +++ b/flow_action_components/String Normaliser/README.md @@ -0,0 +1,24 @@ +# String Normaliser +String Normaliser is an utility class initially developed to replace accented characters on names (diacritical) into their ASCII equivalents. +eg. Mélanie > Melanie, François > Francois, José > Jose, Iñaki > Inaki​, João > Joao, etc. +(all strings are returned in lower case to increase processing time by reducing the number of characters to iterate). + +Additional methods have been added to extend its functionality such as: +- Removing special characters keeping alphanumeric only, eg. O'brian > Obrian +- Replacing special characters with empty spaces, eg. O'brian > O brian +- Returning using proper case (first letter capitalised), eg. joe doe > Joe Doe +- Removing all spaces + +Specially useful during duplicate detection, eg when comparing Records in Matching Rules by populating Custom fields (eg. FirstNameASCII__c, LastNameASCII__c), +or when searching existing Contacts/Leads in the Query element and avoid duplicates by creating a new record. + +Custom fields can be populated by a Before-Save Flow or by an Apex Trigger when names are edited or a record is created. +And a Batch Job can be implemented to update all the existing records in the Org to populate the custom fields. +(The utility class is separated from the Invocable Action to allow using in Apex, although the Test Class includes both). + +Current diacritics values can be expanded declarative by moving the Map to a Custom Metadata or Custom Settings. + +Sandbox Installation: https://test.salesforce.com/packaging/installPackage.apexp?p0=04tJ7000000D8xn +Production Installation: https://login.salesforce.com/packaging/installPackage.apexp?p0=04tJ7000000D8xn + +Written by: Jose De Oliveira diff --git a/flow_action_components/String Normaliser/StringNormaliser.cls b/flow_action_components/String Normaliser/StringNormaliser.cls new file mode 100644 index 000000000..f54e2b78a --- /dev/null +++ b/flow_action_components/String Normaliser/StringNormaliser.cls @@ -0,0 +1,98 @@ +/* + * Created by: Jose De Oliveira + * + * Description: + * This utility class is intented to help names comparison by returning a normalized version of the name (ASCII), + * from the accented original version (diacritical), eg. Mélanie, François, José, Iñaki​, João, João, etc. + * This method returns lower case to reduce the size of the Map, so another method allows normalising with Proper case + * where the first letter of every word is capitalized. + * + * It also offers other utility methods such as removing white spaces and special characters to return alphanumeric only. + * + * It can be used in Custom fields (eg. FirstNameASCII__c, LastNameASCII__c) that matching rules can read when performing duplicate detection. + * It can also be used in flows from an Apex Action to perform the comparison during runtime. + * (Custom fields are recomended to avoid adding processing time. + * Those fields can be populated by a Before-Save flow when names are edited - or a record is created) +*/ + +public without sharing class StringNormaliser { + + // Replaces Diacritical characters with their ASCII version + public static String removeDiacritics(String originalString) { + + if (!String.isBlank(originalString)) { + + // To do: Move Map to Custom Metadata Types to allow declarative changes + final Map diacriticalASCII_MAP = new Map{ + 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'ą' => 'a', + 'ç' => 'c', 'ć' => 'c', 'č' => 'c', + 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ę' => 'e', 'ð' => 'e', + 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', + 'ñ' => 'n', 'ń' => 'n', + 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o', + 'ś' => 's', 'š' => 's', + 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', + 'ý' => 'y', 'ÿ' => 'y', + 'ź' => 'z', 'ż' => 'z', 'ž' => 'z' + }; + + // Convert to lower case to minimise the size of the Map by avoiding Capital letters + String convertedString = originalString.toLowerCase(); + + //To do: Iterate thru String rather than the Map Key Set + for (String diacritical : diacriticalASCII_MAP.keySet()) { + convertedString = convertedString.replace(diacritical, diacriticalASCII_MAP.get(diacritical)); + } + + return convertedString; + + } else { + return null; + } + } + + public static String convertToProperCase(String originalString) { + if (!String.isBlank(originalString)) { + + List orgWordsCollection = originalString.toLowerCase().split(' '); + List propWordsCollection = new List(); + + for (String w :orgWordsCollection) { + if (w.length() > 0) { + String properWord = w.substring(0, 1).toUpperCase() + w.substring(1); + propWordsCollection.add(properWord); + } + } + + return String.join(propWordsCollection, ' '); + + } else { + return null; + } + } + + public static String removeSpecialCharacters(String originalString) { + if (!String.isBlank(originalString)) { + return originalString.replaceAll('[^a-z0-9]', ''); + } else { + return null; + } + } + + public static String replaceSpecialCharactersForSpace(String originalString) { + if (!String.isBlank(originalString)) { + return originalString.replaceAll('[^a-z0-9]', ' ').trim(); + } else { + return null; + } + } + + public static String removeAllWhiteSpaces(String originalString) { + if (!String.isBlank(originalString)) { + return originalString.replaceAll('\\s+', ''); + } else { + return null; + } + } + +} \ No newline at end of file diff --git a/flow_action_components/String Normaliser/StringNormaliser.cls-meta.xml b/flow_action_components/String Normaliser/StringNormaliser.cls-meta.xml new file mode 100644 index 000000000..5f399c3cc --- /dev/null +++ b/flow_action_components/String Normaliser/StringNormaliser.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + diff --git a/flow_action_components/String Normaliser/StringNormaliserInvocableAction.cls b/flow_action_components/String Normaliser/StringNormaliserInvocableAction.cls new file mode 100644 index 000000000..bc16e5a8e --- /dev/null +++ b/flow_action_components/String Normaliser/StringNormaliserInvocableAction.cls @@ -0,0 +1,68 @@ +public without sharing class StringNormaliserInvocableAction { + + public class RequestWrapper { + @InvocableVariable(label='String 1' description='eg. First Name' required=false) + public String requestString1; + + @InvocableVariable(label='String 2' description='eg. Last Name' required=true) + public String requestString2; + + @InvocableVariable(label='Apply Proper Case' description='Capitalizes the first letter of every word' required=false) + public Boolean doProperCase = false; + + @InvocableVariable(label='Remove special characters' description='Removes any non-alphanumeric character' required=false) + public Boolean doAlphanumeric = false; + + @InvocableVariable(label='Replace special characters for a space' description='Replaces special characters for a space' required=false) + public Boolean doAlphanumericWithSpace = false; + + @InvocableVariable(label='Remove spaces' description='Removes all white or empty spaces giving a consecutive string' required=false) + public Boolean doNoSpaces = false; + + } + + public class ResultWrapper { + @InvocableVariable + public String resultString1; + + @InvocableVariable + public String resultString2; + } + + @InvocableMethod(label='String Normaliser' description='Replaces Diacritical characters (eg. accented names) with their ASCII version, removes special characters, and replaces whitespace. All in lowercase.' category='Utilities') + public static List normaliseText(List reqCollection) { + + List resultList = new List(); + + // Iterate through each CleanNameRequestWrapper in the input list + for (RequestWrapper r : reqCollection) { + + ResultWrapper result = new ResultWrapper(); + + String convertedString1 = StringNormaliser.removeDiacritics(r.requestString1); + String convertedString2 = StringNormaliser.removeDiacritics(r.requestString2); + + if(r.doAlphanumericWithSpace) { + convertedString1 = StringNormaliser.replaceSpecialCharactersForSpace(convertedString1); + convertedString2 = StringNormaliser.replaceSpecialCharactersForSpace(convertedString2); + } + if(r.doAlphanumeric) { + convertedString1 = StringNormaliser.removeSpecialCharacters(convertedString1); + convertedString2 = StringNormaliser.removeSpecialCharacters(convertedString2); + } + if(r.doProperCase) { + convertedString1 = StringNormaliser.convertToProperCase(convertedString1); + convertedString2 = StringNormaliser.convertToProperCase(convertedString2); + } + if(r.doNoSpaces) { + convertedString1 = StringNormaliser.removeAllWhiteSpaces(convertedString1); + convertedString2 = StringNormaliser.removeAllWhiteSpaces(convertedString2); + } + + result.resultString1 = convertedString1; + result.resultString2 = convertedString2; + resultList.add(result); + } + return resultList; + } +} \ No newline at end of file diff --git a/flow_action_components/String Normaliser/StringNormaliserInvocableAction.cls-meta.xml b/flow_action_components/String Normaliser/StringNormaliserInvocableAction.cls-meta.xml new file mode 100644 index 000000000..5f399c3cc --- /dev/null +++ b/flow_action_components/String Normaliser/StringNormaliserInvocableAction.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + diff --git a/flow_action_components/String Normaliser/StringNormaliserTest.cls b/flow_action_components/String Normaliser/StringNormaliserTest.cls new file mode 100644 index 000000000..a6231317c --- /dev/null +++ b/flow_action_components/String Normaliser/StringNormaliserTest.cls @@ -0,0 +1,135 @@ +@isTest +public with sharing class StringNormaliserTest { + @isTest + public static void testDiacritical() { + + String textToNormalise = 'àáâäãåąèéêëęðçćìíîïòóôöõøùúûüÿýžźżñńšś'; + StringNormaliserInvocableAction.RequestWrapper req = new StringNormaliserInvocableAction.RequestWrapper(); + String errorMessage; + + // Test with all diacritical characters + req.requestString1 = textToNormalise; + req.requestString2 = textToNormalise; + List results = + StringNormaliserInvocableAction.normaliseText(new List{req}); + errorMessage = 'The normalised string did not match the expected output for repeated diacritical characters.'; + Assert.areEqual('aaaaaaaeeeeeecciiiioooooouuuuyyzzznnss', results[0].resultString1, errorMessage); + Assert.areEqual('aaaaaaaeeeeeecciiiioooooouuuuyyzzznnss', results[0].resultString2, errorMessage); + + // Repeated diacritical characters + textToNormalise = 'ààááââ'; + req.requestString1 = textToNormalise; + req.requestString2 = textToNormalise; + List results2 = + StringNormaliserInvocableAction.normaliseText(new List{req}); + errorMessage = 'Diacritical characters should be replaced by their ASCII equivalent.'; + Assert.areEqual('aaaaaa', results2[0].resultString1, errorMessage); + Assert.areEqual('aaaaaa', results2[0].resultString2, errorMessage); + + // Test Capital with diacritical characters + textToNormalise = 'ÁÃAAÃÇČ'; + req.requestString1 = textToNormalise; + req.requestString2 = textToNormalise; + List results3 = + StringNormaliserInvocableAction.normaliseText(new List{req}); + errorMessage = 'The normalised string did not match the expected output for capital case diacritical characters.'; + Assert.areEqual('aaaaacc', results3[0].resultString1, errorMessage); + Assert.areEqual('aaaaacc', results3[0].resultString2, errorMessage); + } + + @isTest + public static void testNoSpaces() { + StringNormaliserInvocableAction.RequestWrapper req = new StringNormaliserInvocableAction.RequestWrapper(); + req.requestString1 = ' Jo h n '; + req.requestString2 = ' D o e '; + req.doNoSpaces = true; + + List results = + StringNormaliserInvocableAction.normaliseText(new List{req}); + + String errorMessage = 'The normalised string should return with no spaces'; + Assert.areEqual('john', results[0].resultString1, errorMessage); + Assert.areEqual('doe', results[0].resultString2, errorMessage); + } + + @isTest + public static void testProperCase() { + StringNormaliserInvocableAction.RequestWrapper req = new StringNormaliserInvocableAction.RequestWrapper(); + req.requestString1 = 'jOhN aNgeL'; + req.requestString2 = 'dOe sMIth'; + req.doProperCase = true; + + List results = + StringNormaliserInvocableAction.normaliseText(new List{req}); + + String errorMessage = 'The normalised string did not return with correct capitalisation of firts letter capital and remaining lower case'; + Assert.areEqual('John Angel', results[0].resultString1, errorMessage); + Assert.areEqual('Doe Smith', results[0].resultString2, errorMessage); + } + + @isTest + public static void testAlphanumeric() { + StringNormaliserInvocableAction.RequestWrapper req = new StringNormaliserInvocableAction.RequestWrapper(); + req.requestString1 = 'Jo@hn!'; + req.requestString2 = 'Do#e$'; + req.doAlphanumeric = true; + + List results = + StringNormaliserInvocableAction.normaliseText(new List{req}); + + String errorMessage = 'The normalised string did not remove all special characters returning alphanumeric only'; + Assert.areEqual('john', results[0].resultString1, errorMessage); + Assert.areEqual('doe', results[0].resultString2, errorMessage); + } + + @isTest + public static void testStandardNormalisation() { + StringNormaliserInvocableAction.RequestWrapper req = new StringNormaliserInvocableAction.RequestWrapper(); + req.requestString1 = 'jOhN !aNgeL,'; + req.requestString2 = 'dOe o\'hANNah'; + req.doProperCase = true; + req.doAlphanumericWithSpace = true; + + List results = + StringNormaliserInvocableAction.normaliseText(new List{req}); + + String errorMessage = 'The normalised string did not return with correct capitalisation of firts letter capital and remaining lower case and replace all special characters by a spaces, returning alphanumeric only'; + Assert.areEqual('John Angel', results[0].resultString1, errorMessage); + Assert.areEqual('Doe O Hannah', results[0].resultString2, errorMessage); + } + + @isTest + public static void testAlphanumericWithSpace() { + StringNormaliserInvocableAction.RequestWrapper req = new StringNormaliserInvocableAction.RequestWrapper(); + req.requestString1 = 'John!@2 Spaces'; + req.requestString2 = 'Doe_O\'hannah'; + req.doAlphanumericWithSpace = true; + + List results = + StringNormaliserInvocableAction.normaliseText(new List{req}); + + String errorMessage = 'The normalised string did not replace all special characters by a space, returning alphanumeric only'; + Assert.areEqual('john 2 spaces', results[0].resultString1, errorMessage); + Assert.areEqual('doe o hannah', results[0].resultString2, errorMessage); + } + + @isTest + public static void testEmptyString() { + + // Should return null instead of an Exception + StringNormaliserInvocableAction.RequestWrapper req = new StringNormaliserInvocableAction.RequestWrapper(); + req.requestString1 = ''; + req.requestString2 = null; + req.doAlphanumericWithSpace = true; + req.doAlphanumeric = true; + req.doNoSpaces = true; + req.doProperCase = true; + + List results = + StringNormaliserInvocableAction.normaliseText(new List{req}); + + String errorMessage = 'The normalised string did not return null'; + Assert.isNull(results[0].resultString1, errorMessage); + Assert.isNull(results[0].resultString2, errorMessage); + } +} \ No newline at end of file diff --git a/flow_action_components/String Normaliser/StringNormaliserTest.cls-meta.xml b/flow_action_components/String Normaliser/StringNormaliserTest.cls-meta.xml new file mode 100644 index 000000000..5f399c3cc --- /dev/null +++ b/flow_action_components/String Normaliser/StringNormaliserTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + diff --git a/flow_action_components/String Normaliser/String_Normaliser_Demo_Screen.flow-meta.xml b/flow_action_components/String Normaliser/String_Normaliser_Demo_Screen.flow-meta.xml new file mode 100644 index 000000000..d75be0859 --- /dev/null +++ b/flow_action_components/String Normaliser/String_Normaliser_Demo_Screen.flow-meta.xml @@ -0,0 +1,252 @@ + + + 63.0 + false + + ScreenProgressIndicator + + {"location":"top","type":"simple"} + + + Shows how normalised Strings will look like when applying different criteria + Default + + fxBool_areAlphanumericTrue + Boolean + {!scr_chkBx_doAlphanumericWithSpace} || {!scr_chkBx_doAlphanumeric} + + + fxBool_isNoSpaces + Boolean + {!scr_chkBx_doNoSpaces} || {!scr_chkBx_doAlphanumeric} + + String Normaliser Demo Screen {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + Flow + + Initial_Screen + + 176 + 134 + + subflow_stringNormaliser + jdo__String_Normaliser_Demo_Subflow + flow + + in_vBool_doAlphanumeric + + scr_chkBx_doAlphanumeric + + + + in_vBool_doAlphanumericWithSpace + + scr_chkBx_doAlphanumericWithSpace + + + + in_vBool_doNoSpaces + + scr_chkBx_doNoSpaces + + + + in_vBool_doProperCase + + scr_chkBx_doProperCase + + + + in_vTxt_string1 + + scrTxtInput_string1 + + + + in_vTxt_string2 + + scrTxtInput_string2 + + + + jdo__String_Normaliser_Demo_Subflow + + true + true + true + + Initial_Screen_Section1 + RegionContainer + + Initial_Screen_Section1_Column1 + Region + + scrTxtInput_string1 + String + + fÎrŠt £$ %^ Nâmë !$. &. $^ + + String 1 to normalise + InputField + <p>eg. First Name</p> + UseStoredValues + false + + + scrTxtInput_string2 + String + + láśt £$ %^ Nâmë !£$ %^ $. &. $^ + + String 2 to normalise + InputField + <p><span style="color: rgb(68, 68, 68); background-color: rgb(255, 255, 255);">eg. Last Name</span></p> + UseStoredValues + true + + + actionButton_normalise + flowruntime:actionButtonField + ComponentInstance + + label + + Normalise + + + + isSuccess + + subflow_stringNormaliser.IsSuccess + + + + inProgress + + subflow_stringNormaliser.InProgress + + + + errorMessage + + subflow_stringNormaliser.ErrorMessage + + + UseStoredValues + true + true + + + width + + 6 + + + false + + + Initial_Screen_Section1_Column2 + Region + + scr_chkBx_doProperCase + Boolean + + true + + Return Proper Case + InputField + UseStoredValues + true + + + scr_chkBx_doAlphanumeric + Boolean + Remove Special Characters + InputField + UseStoredValues + + scr_chkBx_doAlphanumericWithSpace + + true + + + scr_chkBx_doAlphanumericWithSpace + Boolean + + true + + Replace Special Characters for Spaces + InputField + UseStoredValues + + fxBool_isNoSpaces + + true + + + scr_chkBx_doNoSpaces + Boolean + + scr_chkBx_doAlphanumeric + + Remove All Spaces + InputField + UseStoredValues + + fxBool_areAlphanumericTrue + + true + + + width + + 6 + + + false + + false + SectionWithoutHeader + + + scrTxtDisplay_showResults + <p><strong style="font-size: 18px;">Results:</strong></p><p><strong>String 1: </strong>{!subflow_stringNormaliser.Results.out_vTxt_normalisedString1}</p><p><strong style="background-color: rgb(255, 255, 255); color: rgb(68, 68, 68);">String 2: </strong><span style="background-color: rgb(255, 255, 255); color: rgb(68, 68, 68);">{!subflow_stringNormaliser.Results.out_vTxt_normalisedString2}</span></p> + DisplayText + + true + true + + flow__screenfieldclick + actionButton_normalise + + subflow_stringNormaliser + + + + + 50 + 0 + + Initial_Screen + + + Active + diff --git a/flow_action_components/String Normaliser/String_Normaliser_Demo_Subflow.flow-meta.xml b/flow_action_components/String Normaliser/String_Normaliser_Demo_Subflow.flow-meta.xml new file mode 100644 index 000000000..918e25ced --- /dev/null +++ b/flow_action_components/String Normaliser/String_Normaliser_Demo_Subflow.flow-meta.xml @@ -0,0 +1,164 @@ + + + + Normalise_String + + 176 + 134 + StringNormaliserInvocableAction + apex + CurrentTransaction + + doAlphanumeric + + in_vBool_doAlphanumeric + + + + doAlphanumericWithSpace + + in_vBool_doAlphanumericWithSpace + + + + doNoSpaces + + in_vBool_doNoSpaces + + + + doProperCase + + in_vBool_doProperCase + + + + requestString1 + + in_vTxt_string1 + + + + requestString2 + + in_vTxt_string2 + + + StringNormaliserInvocableAction + 0 + + out_vTxt_normalisedString1 + resultString1 + + + out_vTxt_normalisedString2 + resultString2 + + + 63.0 + false + Used in Demo Screen Flow, handles the Button Action to normalise the Strings + Default + String Normaliser Demo Subflow {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + 50 + 0 + + Normalise_String + + + Active + + Removes special characters + in_vBool_doAlphanumeric + Boolean + false + true + false + + false + + + + in_vBool_doAlphanumericWithSpace + Boolean + false + true + false + + false + + + + Removes all empty spaces + in_vBool_doNoSpaces + Boolean + false + true + false + + false + + + + Capitalises the first letter of every word + in_vBool_doProperCase + Boolean + false + true + false + + false + + + + eg. First Name + in_vTxt_string1 + String + false + true + false + + + eg. Last Name + in_vTxt_string2 + String + false + true + false + + + out_vTxt_normalisedString1 + String + false + false + true + + + out_vTxt_normalisedString2 + String + false + false + true + +