6

I am trying to copy a Rich Text Field (32k) into multiple Rich Text Field (2k). I get this error:

System.StringException: Ending position out of bounds:2000.

Code

trigger UpdateCaseResponse on Case (before update) {
    for(Case c: trigger.new ){
        if (c.Response__c != null)        
        if(c.Response__c != Trigger.oldMap.get(c.Id).Response__c )

        try{
            c.Response_Part1__c = c.Response__c.substring(0,2000);
            c.Response_Part2__c = c.Response__c.substring(2001,4000);
        }       
        catch (System.NullPointerException e) {


        }
    }
}
  • 4
    problem is that your string may be actually shorter than 2k. That's why it says out of bounds. Check total length of string first, and then use 2k or total length, whatever is longer – kurunve May 10 '16 at 13:27
8

As an alternative to using substring, you can also use Pattern/Matcher:

SObjectField[] fields = new SObjectField[] {  // List your fields...
    Case.Response_Part1__c, Case.Response_Part2__c,
    Case.Response_Part3__c, Case.Response_Part4__c // ...
};
Pattern p = Pattern.compile('(?s).{1,2000}');  // Read up to 2k at a time

for(Case record: Trigger.new) {
    if(record.Response__c == null || record.Response__c == Trigger.oldMap.get(record.Id).Response__c) {
        continue;
    }
    Matcher m = p.matcher(c.Response__c);
    for(Integer fieldToUse = 0; fieldToUse < fields.size() && m.find(); fieldToUse++) {
        record.put(fields[fieldToUse], m.group(0));
    }
}

This avoids substring exceptions and a particularly nasty set of if statements.

  • 1
    @AdrianLarson Works with strings up to 1,000,000 characters. – sfdcfox May 10 '16 at 17:30
  • 2
    I think you need to match newline characters. So (?s).{1,2000} – Adrian Larson May 10 '16 at 18:28
  • 1
    It would be interesting to profile the performance differences (between our answers). I'm having a hard time of it currently but may revisit later. – Adrian Larson May 10 '16 at 18:34
  • 4
    @AdrianLarson Substring: about 0.01ms per 2k string within the larger string, and 0.02ms per 2k string with regexp. Tested by building a million character string, and processing it 100 times, which was ~500ms vs ~1000ms over several runs. While substring is twice as fast, either method should be acceptable in the context of a trigger, since even 32k strings * 200 is only 6.4 million characters compared to my test of 100,000,000 characters. – sfdcfox May 10 '16 at 18:53
  • 1
    @Khyber Just clear all the fields beforehand (for(sobject field: fields) { record.put(field, null); }), probably right before the for(integer fieldToUse = 0... part. – sfdcfox May 15 '16 at 2:34
8

Note that you do not need to skip an index between substrings. Your current pattern actually loses data. When you do substring(2001, 4000), you start at character #2002. When you do substring(0, 2000), you stop at character #2000 (whose index was 1999). So you lose character #2001 whose index is 2000.

This can be demonstrated simply:

String text = '1234567890';
system.assertEquals('12345', text.substring(0, 5);
system.assertEquals('7890', text.substring(6, 10);

From the documentation (emphasis mine):

  • substring(startIndex, endIndex)
    Returns a new String that begins with the character at the specified zero-based startIndex and extends to the character at endIndex - 1.

If instead the substring extended to endIndex, you would need to do substring(0, 1999), etc.


Solution

You need to split the data out into up to 16 chunks. To make it scalable, I would do something like the following in an Apex Class and then call it from your trigger.

public with sharing class CaseServices
{
    static final List<SObjectField> responseChunkFields = new List<SObjectField>
    {
        Case.Response_Part1__c, Case.Response_Part2__c,
        Case.Response_Part3__c, Case.Response_Part4__c,
        Case.Response_Part5__c, Case.Response_Part6__c,
        Case.Response_Part7__c, Case.Response_Part8__c,
        Case.Response_Part9__c, Case.Response_Part10__c,
        Case.Response_Part11__c, Case.Response_Part12__c,
        Case.Response_Part13__c, Case.Response_Part14__c,
        Case.Response_Part15__c, Case.Response_Part16__c,
    };
    public static void chunkResponses(List<Case> records)
    {
        for (Case record : records) chunkResponse(record);
    }
    static void chunkResponse(Case record)
    {
        if (String.isBlank(record.Response__c)) return;
        Integer length = record.Response__c.length;
        Integer chunks = (Integer)(length/2000.0).round(RoundingMode.UP);
        for (Integer i = 0; i < chunks; i++)
        {
            Integer j = (i == chunks - 1) ? length : 2000*(i+1);
            String chunk = record.Response__c.substring(2000*i, j);
            record.put(responseChunkFields[i], chunk);
        }
    }
}
  • 2
    I'd cache the results from getChunks for performance reasons: for(Integer i = 0, size = getChunks(record.Response__c); i < size; i++)... – sfdcfox May 10 '16 at 17:04
  • 2
    Also, you need to make sure that ending position isn't out of bounds, so you can't just use 2000*(i+1). – sfdcfox May 10 '16 at 17:07
  • Doesn't caching it just increase heap? Only calculates once I think. Will fix ending position out of bounds though, yes. -- Ah I see it now. – Adrian Larson May 10 '16 at 17:17
  • 1
    Yes, the heap would increase by about 4-8 bytes, I think, but you save on repeated division and rounding, which can add up. And, no, the loop condition has to execute the function every time, since it doesn't know that length won't change. – sfdcfox May 10 '16 at 17:20
  • Hey Adrian, thanks a lot for your response, i just tested your code, same error 'Ending position out of bounds: 8000' on line 20. – Khyber May 11 '16 at 0:14
4

Reason :-

The length of string is less than 2000 which caused an error.

Resolution:-

Please use length() function of String class to solve the issue. Please use the below code:-

trigger UpdateCaseResponse on Case (before update) {        
    for(Case c: trigger.new ){        
        Integer resLength =  c.Subject.length();        
        if (c.Response__c != null)        
        if(c.Response__c != Trigger.oldMap.get(c.Id).Response__c )            
        if( resLength <= 2000){
            c.Response_Part1__c = c.Response__c.substring(0,resLength);
        }                       
        else if( resLength > 2000 &&  resLength <= 4000 ){              
            c.Response_Part1__c = c.Response__c.substring(0,2000);
            c.Response_Part2__c = c.Response__c.substring(2001,resLength);
        }                               
    }  
}
  • 3
    Most try/catch code does more harm than good including in this case; suggest you remove that. – Keith C May 10 '16 at 14:21
  • 3
    Also, that if statement would be a beast written that way. Lots of copy-pasting, and prone to simple mistakes. – sfdcfox May 10 '16 at 17:04
  • Hey Shanu, thanks a lot for your response, i appreciate it, i just tried your code, by the time i was down to part 4 field, it was cumbersome. – Khyber May 11 '16 at 0:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.