sf-agent-framework
Version:
AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction
666 lines (525 loc) • 18.1 kB
Markdown
# Common Salesforce Data Transformations
## Overview
Data transformations are critical when importing, exporting, or synchronizing
data with Salesforce. This document covers common transformation patterns,
formulas, and best practices for data manipulation.
## String Transformations
### Case Transformations
```apex
// Proper case transformation
public static String toProperCase(String input) {
if (String.isBlank(input)) return input;
List<String> words = input.toLowerCase().split(' ');
List<String> result = new List<String>();
for (String word : words) {
if (word.length() > 0) {
result.add(word.substring(0, 1).toUpperCase() + word.substring(1));
}
}
return String.join(result, ' ');
}
// Title case with exceptions
public static String toTitleCase(String input) {
Set<String> exceptions = new Set<String>{
'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for',
'if', 'in', 'nor', 'of', 'on', 'or', 'so', 'the',
'to', 'up', 'yet'
};
List<String> words = input.toLowerCase().split(' ');
List<String> result = new List<String>();
for (Integer i = 0; i < words.size(); i++) {
String word = words[i];
if (i == 0 || i == words.size() - 1 || !exceptions.contains(word)) {
word = word.substring(0, 1).toUpperCase() + word.substring(1);
}
result.add(word);
}
return String.join(result, ' ');
}
```
### Text Cleaning
```apex
// Remove special characters
public static String cleanText(String input) {
// Remove non-printable characters
String cleaned = input.replaceAll('[\x00-\x1F\x7F]', '');
// Normalize whitespace
cleaned = cleaned.replaceAll('\s+', ' ');
// Trim
return cleaned.trim();
}
// Remove HTML tags
public static String stripHtml(String input) {
return input.replaceAll('<[^>]+>', '');
}
// Truncate with ellipsis
public static String truncate(String input, Integer maxLength) {
if (input == null || input.length() <= maxLength) {
return input;
}
return input.substring(0, maxLength - 3) + '...';
}
```
### String Parsing
```apex
// Extract email domain
public static String extractDomain(String email) {
if (email != null && email.contains('@')) {
return email.substring(email.indexOf('@') + 1).toLowerCase();
}
return null;
}
// Parse full name
public static Map<String, String> parseName(String fullName) {
Map<String, String> result = new Map<String, String>();
if (String.isBlank(fullName)) {
return result;
}
List<String> parts = fullName.trim().split('\\s+');
if (parts.size() >= 1) {
result.put('firstName', parts[0]);
}
if (parts.size() >= 2) {
result.put('lastName', parts[parts.size() - 1]);
}
if (parts.size() >= 3) {
List<String> middleParts = new List<String>();
for (Integer i = 1; i < parts.size() - 1; i++) {
middleParts.add(parts[i]);
}
result.put('middleName', String.join(middleParts, ' '));
}
return result;
}
```
## Number Transformations
### Formatting Numbers
```apex
// Format currency
public static String formatCurrency(Decimal amount, String currencyCode) {
if (amount == null) return '';
Map<String, String> symbols = new Map<String, String>{
'USD' => '$',
'EUR' => '€',
'GBP' => '£',
'JPY' => '¥'
};
String symbol = symbols.get(currencyCode) != null ? symbols.get(currencyCode) : currencyCode + ' ';
// Format with thousands separator
String formatted = amount.format();
return symbol + formatted;
}
// Format percentage
public static String formatPercentage(Decimal value, Integer decimalPlaces) {
if (value == null) return '';
Decimal percentage = value * 100;
return percentage.setScale(decimalPlaces).format() + '%';
}
// Round to nearest
public static Decimal roundToNearest(Decimal value, Decimal nearest) {
if (value == null || nearest == null || nearest == 0) return value;
return Math.round(value / nearest) * nearest;
}
```
### Number Parsing
```apex
// Parse currency string
public static Decimal parseCurrency(String currencyString) {
if (String.isBlank(currencyString)) return null;
// Remove currency symbols and commas
String cleaned = currencyString.replaceAll('[^0-9.-]', '');
try {
return Decimal.valueOf(cleaned);
} catch (Exception e) {
return null;
}
}
// Parse percentage
public static Decimal parsePercentage(String percentString) {
if (String.isBlank(percentString)) return null;
String cleaned = percentString.replaceAll('[^0-9.-]', '');
try {
return Decimal.valueOf(cleaned) / 100;
} catch (Exception e) {
return null;
}
}
```
## Date and Time Transformations
### Date Formatting
```apex
// Format date in various formats
public static String formatDate(Date inputDate, String format) {
if (inputDate == null) return '';
DateTime dt = DateTime.newInstance(inputDate.year(), inputDate.month(), inputDate.day());
switch on format {
when 'ISO' {
return dt.format('yyyy-MM-dd');
}
when 'US' {
return dt.format('MM/dd/yyyy');
}
when 'EU' {
return dt.format('dd/MM/yyyy');
}
when 'LONG' {
return dt.format('MMMM d, yyyy');
}
when else {
return dt.format(format);
}
}
}
// Convert between timezones
public static DateTime convertTimezone(DateTime dt, String fromTz, String toTz) {
// Get timezone offsets
TimeZone fromTimeZone = TimeZone.getTimeZone(fromTz);
TimeZone toTimeZone = TimeZone.getTimeZone(toTz);
Integer fromOffset = fromTimeZone.getOffset(dt);
Integer toOffset = toTimeZone.getOffset(dt);
// Calculate difference in milliseconds
Integer difference = toOffset - fromOffset;
return dt.addSeconds(difference / 1000);
}
```
### Date Calculations
```apex
// Business days calculation
public static Integer getBusinessDaysBetween(Date startDate, Date endDate) {
Integer businessDays = 0;
Date currentDate = startDate;
while (currentDate <= endDate) {
DateTime dt = DateTime.newInstance(currentDate.year(), currentDate.month(), currentDate.day());
String dayOfWeek = dt.format('E');
if (dayOfWeek != 'Sat' && dayOfWeek != 'Sun') {
businessDays++;
}
currentDate = currentDate.addDays(1);
}
return businessDays;
}
// Add business days
public static Date addBusinessDays(Date startDate, Integer daysToAdd) {
Date resultDate = startDate;
Integer addedDays = 0;
while (addedDays < daysToAdd) {
resultDate = resultDate.addDays(1);
DateTime dt = DateTime.newInstance(resultDate.year(), resultDate.month(), resultDate.day());
String dayOfWeek = dt.format('E');
if (dayOfWeek != 'Sat' && dayOfWeek != 'Sun') {
addedDays++;
}
}
return resultDate;
}
```
## Phone Number Transformations
### Phone Formatting
```apex
public class PhoneFormatter {
// Format US phone numbers
public static String formatUSPhone(String phone) {
if (String.isBlank(phone)) return phone;
// Remove all non-numeric characters
String cleaned = phone.replaceAll('[^0-9]', '');
// Handle different lengths
if (cleaned.length() == 10) {
return String.format('({0}) {1}-{2}', new List<String>{
cleaned.substring(0, 3),
cleaned.substring(3, 6),
cleaned.substring(6)
});
} else if (cleaned.length() == 11 && cleaned.startsWith('1')) {
return '+1 ' + formatUSPhone(cleaned.substring(1));
} else if (cleaned.length() == 7) {
return String.format('{0}-{1}', new List<String>{
cleaned.substring(0, 3),
cleaned.substring(3)
});
}
return phone;
}
// Format international phone numbers
public static String formatInternational(String phone, String countryCode) {
if (String.isBlank(phone)) return phone;
Map<String, String> formats = new Map<String, String>{
'GB' => '+44 {0} {1} {2}', // UK
'DE' => '+49 {0} {1}', // Germany
'FR' => '+33 {0} {1} {2} {3} {4}', // France
'AU' => '+61 {0} {1} {2} {3}' // Australia
};
// Implementation varies by country
return phone; // Simplified
}
}
```
## Address Transformations
### Address Standardization
```apex
public class AddressStandardizer {
// Standardize US addresses
public static Map<String, String> standardizeUSAddress(String fullAddress) {
Map<String, String> result = new Map<String, String>();
// Common abbreviations
Map<String, String> abbreviations = new Map<String, String>{
'street' => 'St',
'avenue' => 'Ave',
'boulevard' => 'Blvd',
'drive' => 'Dr',
'lane' => 'Ln',
'road' => 'Rd',
'north' => 'N',
'south' => 'S',
'east' => 'E',
'west' => 'W'
};
// Parse address components
// This is simplified - real implementation would be more complex
List<String> lines = fullAddress.split('\n');
if (lines.size() >= 1) {
result.put('street', standardizeStreet(lines[0], abbreviations));
}
if (lines.size() >= 2) {
parseCity StateZip(lines[1], result);
}
return result;
}
// Parse city, state, zip
private static void parseCityStateZip(String line, Map<String, String> result) {
// Match pattern: City, ST 12345
Pattern p = Pattern.compile('([^,]+),\\s*([A-Z]{2})\\s+(\\d{5}(-\\d{4})?)$');
Matcher m = p.matcher(line);
if (m.find()) {
result.put('city', m.group(1).trim());
result.put('state', m.group(2));
result.put('postalCode', m.group(3));
}
}
}
```
## Email Transformations
### Email Validation and Cleaning
```apex
public class EmailTransformer {
// Validate and clean email
public static String cleanEmail(String email) {
if (String.isBlank(email)) return null;
// Trim and lowercase
email = email.trim().toLowerCase();
// Remove common typos
email = email.replace('..', '.');
email = email.replace('.@', '@');
email = email.replace('@.', '@');
// Validate format
Pattern emailPattern = Pattern.compile(
'^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'
);
if (emailPattern.matcher(email).matches()) {
return email;
}
return null;
}
// Extract name from email
public static Map<String, String> extractNameFromEmail(String email) {
Map<String, String> result = new Map<String, String>();
if (email != null && email.contains('@')) {
String localPart = email.substring(0, email.indexOf('@'));
// Try common patterns
if (localPart.contains('.')) {
List<String> parts = localPart.split('\\.');
result.put('firstName', capitalize(parts[0]));
result.put('lastName', capitalize(parts[parts.size() - 1]));
} else if (localPart.contains('_')) {
List<String> parts = localPart.split('_');
result.put('firstName', capitalize(parts[0]));
result.put('lastName', capitalize(parts[parts.size() - 1]));
}
}
return result;
}
}
```
## Picklist Value Transformations
### Picklist Mapping
```apex
public class PicklistMapper {
// Map external values to Salesforce picklist values
public static String mapToPicklist(String externalValue, String objectName, String fieldName) {
// Get picklist values
Schema.DescribeFieldResult fieldResult =
Schema.getGlobalDescribe().get(objectName).getDescribe()
.fields.getMap().get(fieldName).getDescribe();
List<Schema.PicklistEntry> picklistValues = fieldResult.getPicklistValues();
// Create mapping
Map<String, String> valueMap = new Map<String, String>();
for (Schema.PicklistEntry pe : picklistValues) {
valueMap.put(pe.getLabel().toLowerCase(), pe.getValue());
valueMap.put(pe.getValue().toLowerCase(), pe.getValue());
}
// Try exact match first
String normalized = externalValue.trim().toLowerCase();
if (valueMap.containsKey(normalized)) {
return valueMap.get(normalized);
}
// Try fuzzy matching
for (String key : valueMap.keySet()) {
if (key.contains(normalized) || normalized.contains(key)) {
return valueMap.get(key);
}
}
return null; // No match found
}
}
```
## Boolean Transformations
### Boolean Parsing
```apex
public static Boolean parseBoolean(String value) {
if (String.isBlank(value)) return null;
Set<String> trueValues = new Set<String>{
'true', 'yes', 'y', '1', 'on', 'active', 'enabled'
};
Set<String> falseValues = new Set<String>{
'false', 'no', 'n', '0', 'off', 'inactive', 'disabled'
};
String normalized = value.trim().toLowerCase();
if (trueValues.contains(normalized)) {
return true;
} else if (falseValues.contains(normalized)) {
return false;
}
return null;
}
```
## ID and Reference Transformations
### ID Conversions
```apex
public class IdTransformer {
// Convert 15-character ID to 18-character
public static String to18CharId(String id15) {
if (String.isBlank(id15) || id15.length() != 15) {
return id15;
}
String suffix = '';
for (Integer i = 0; i < 3; i++) {
Integer flags = 0;
for (Integer j = 0; j < 5; j++) {
String c = id15.substring(i * 5 + j, i * 5 + j + 1);
if (c.isAllUpperCase() && c.isAlpha()) {
flags += 1 << j;
}
}
suffix += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'.substring(flags, flags + 1);
}
return id15 + suffix;
}
// Create external ID from multiple fields
public static String createCompositeKey(List<String> values) {
List<String> cleaned = new List<String>();
for (String value : values) {
if (!String.isBlank(value)) {
cleaned.add(value.trim());
}
}
return String.join(cleaned, '_');
}
}
```
## Bulk Data Transformations
### Batch Transformation Framework
```apex
public class BulkTransformer implements Database.Batchable<SObject> {
private String query;
private TransformationRule rule;
public BulkTransformer(String query, TransformationRule rule) {
this.query = query;
this.rule = rule;
}
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext bc, List<SObject> scope) {
for (SObject record : scope) {
rule.transform(record);
}
update scope;
}
public void finish(Database.BatchableContext bc) {
// Send completion notification
}
}
// Transformation rule interface
public interface TransformationRule {
void transform(SObject record);
}
// Example implementation
public class PhoneStandardizationRule implements TransformationRule {
public void transform(SObject record) {
String phone = (String)record.get('Phone');
if (!String.isBlank(phone)) {
record.put('Phone', PhoneFormatter.formatUSPhone(phone));
}
}
}
```
## Formula Field Transformations
### Common Formula Patterns
```sql
-- Concatenate with null handling
IF(ISBLANK(FirstName),
LastName,
FirstName & ' ' & LastName
)
-- Extract domain from email
IF(CONTAINS(Email, '@'),
MID(Email, FIND('@', Email) + 1, LEN(Email)),
null
)
-- Calculate age from birthdate
IF(ISBLANK(Birthdate),
null,
FLOOR((TODAY() - Birthdate) / 365.25)
)
-- Format phone for display
IF(LEN(Phone) = 10,
'(' & LEFT(Phone, 3) & ') ' & MID(Phone, 4, 3) & '-' & RIGHT(Phone, 4),
Phone
)
-- Conditional picklist mapping
CASE(Status__c,
'New', 'Not Started',
'In Progress', 'Working',
'Complete', 'Finished',
'Unknown'
)
```
## Performance Considerations
### Optimization Strategies
1. **Batch Processing**: Use Database.Batchable for large datasets
2. **Bulkification**: Process records in collections
3. **Lazy Loading**: Transform only when needed
4. **Caching**: Store transformation results
5. **Async Processing**: Use or Queueable
### Memory Management
```apex
// Process large datasets in chunks
public static void transformLargeDataset(List<Id> recordIds) {
Integer chunkSize = 200;
for (Integer i = 0; i < recordIds.size(); i += chunkSize) {
List<Id> chunk = new List<Id>();
for (Integer j = i; j < Math.min(i + chunkSize, recordIds.size()); j++) {
chunk.add(recordIds[j]);
}
processChunk(chunk);
}
}
private static void processChunk(List<Id> recordIds) {
// Process chunk asynchronously
}
```
## Additional Resources
- [Salesforce Formula Functions](https://help.salesforce.com/s/articleView?id=sf.customize_functions.htm)
- [Apex String Methods](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_string.htm)
- [Data Loader Transformation Guide](https://developer.salesforce.com/docs/atlas.en-us.dataLoader.meta/dataLoader/)
- [ETL Best Practices](https://developer.salesforce.com/docs/atlas.en-us.integration_patterns_and_practices.meta/)