Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions flow_action_components/String Normaliser/README.md
Original file line number Diff line number Diff line change
@@ -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
98 changes: 98 additions & 0 deletions flow_action_components/String Normaliser/StringNormaliser.cls
Original file line number Diff line number Diff line change
@@ -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<String, String> diacriticalASCII_MAP = new Map<String, String>{
'à' => '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<String> orgWordsCollection = originalString.toLowerCase().split(' ');
List<String> propWordsCollection = new List<String>();

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;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
@@ -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<ResultWrapper> normaliseText(List<RequestWrapper> reqCollection) {

List<ResultWrapper> resultList = new List<ResultWrapper>();

// 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<status>Active</status>
</ApexClass>
135 changes: 135 additions & 0 deletions flow_action_components/String Normaliser/StringNormaliserTest.cls
Original file line number Diff line number Diff line change
@@ -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<StringNormaliserInvocableAction.ResultWrapper> results =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{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<StringNormaliserInvocableAction.ResultWrapper> results2 =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{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<StringNormaliserInvocableAction.ResultWrapper> results3 =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{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<StringNormaliserInvocableAction.ResultWrapper> results =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{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<StringNormaliserInvocableAction.ResultWrapper> results =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{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<StringNormaliserInvocableAction.ResultWrapper> results =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{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<StringNormaliserInvocableAction.ResultWrapper> results =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{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<StringNormaliserInvocableAction.ResultWrapper> results =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{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<StringNormaliserInvocableAction.ResultWrapper> results =
StringNormaliserInvocableAction.normaliseText(new List<StringNormaliserInvocableAction.RequestWrapper>{req});

String errorMessage = 'The normalised string did not return null';
Assert.isNull(results[0].resultString1, errorMessage);
Assert.isNull(results[0].resultString2, errorMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>63.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading