0

I have some data in an NSString, separated by colons:

@"John:Doe:1970:Male:Dodge:Durango"

I need to limit the total length of this string to 100 characters. But I also need to ensure the correct number of colons are present.

What would be a reasonable to way to truncate the string but also add the extra colons so I can parse it into the correct number of fields on the other side?

For example, if my limit was 18, you would end up with something like this:

@"John:Doe:1970:Ma::"

Here's an updated version of my own latest pass at this. Uses @blinkenlights algorithm:

+ (NSUInteger)occurrencesOfSubstring:(NSString *)substring inString:(NSString *)string {
    // http://stackoverflow.com/a/5310084/878969
    return [string length] - [[string stringByReplacingOccurrencesOfString:substring withString:@""] length] / [substring length];
}

+ (NSString *)truncateString:(NSString *)string toLength:(NSUInteger)length butKeepDelmiter:(NSString *)delimiter {
    if (string.length <= length)
        return string;
    NSAssert(delimiter.length == 1, @"Expected delimiter to be a string containing a single character");
    int numDelimitersInOriginal = [[self class] occurrencesOfSubstring:delimiter inString:string];

    NSMutableString *truncatedString = [[string substringToIndex:length] mutableCopy];
    int numDelimitersInTruncated = [[self class] occurrencesOfSubstring:delimiter inString:truncatedString];
    int numDelimitersToAdd = numDelimitersInOriginal - numDelimitersInTruncated;
    int index = length - 1;
    while (numDelimitersToAdd > 0) { // edge case not handled here
        NSRange nextRange = NSMakeRange(index, 1);
        index -= 1;
        NSString *rangeSubstring = [truncatedString substringWithRange:nextRange];
        if ([rangeSubstring isEqualToString:delimiter])
            continue;
        [truncatedString replaceCharactersInRange:nextRange withString:delimiter];
        numDelimitersToAdd -= 1;
    }
    return truncatedString;
}

Note that I don't think this solution handles the edge case from CRD where the number of delimiters is less than the limit.

The reason I need the correct number of colons is the code on the server will split on colon and expect to get 5 strings back.

You can assume the components of the colon separated string do not themselves contain colons.

funroll
  • 35,925
  • 7
  • 54
  • 59
  • 2
    Show the code you've tried so far. – rmaddy Nov 13 '13 at 19:18
  • I'm not sure if I understood your question. You will truncate your string if it has length > 100 but you also want to add colons to reach length = 100 if it has length < 100? – Raphael Oliveira Nov 13 '13 at 19:22
  • Impossible. The string of 101 colons can not be truncated by this rules. – A-Live Nov 13 '13 at 19:25
  • Side note - why the limit? Are you interfacing with some ancient COBOL system? It's 2013. Servers should be smarter than this. If the server has some limit then the server should enforce the limit, not every client that accesses it. – rmaddy Nov 13 '13 at 19:33
  • @rmaddy The value is getting packed into a field that's not designed to hold it. In an analytics system. And the limit of that field is 100 characters. – funroll Nov 13 '13 at 19:35

2 Answers2

3

Your current algorithm will not produce the correct result when one or more of the characters among the last colonsToAdd is a colon.

You can use this approach instead:

  • Cut the string at 100 characters, and store the characters in an NSMutableString
  • Count the number of colons, and subtract that number from the number that you need
  • Starting at the back of the string, replace non-colon characters with colons until you have the right number of colons.
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • Good point that the second substringToIndex operation could remove colons. – funroll Nov 13 '13 at 19:31
  • This will not work if one of the characters you replace is a colon, you need to check that first, and if it is, replace one more character for every colon in the "replaceable" part – Mark E Nov 13 '13 at 19:40
  • @MarkE The answer does say "replace all **non-colon** characters with colons". I edited for clarity to say when to stop. – Sergey Kalinichenko Nov 13 '13 at 19:42
  • you're right, I think this algorithm would do the job perfectly – Mark E Nov 13 '13 at 19:46
  • @dasblinkenlight Thank you for your algorithm--I think it would work. I'm going to wait to accept though because I was looking for code. – funroll Nov 13 '13 at 20:11
  • 1
    @funroll Why do you need someone else's code, when you have nearly all the code that you need? You've got your `colonsToAdd` calculation, all that's left to do is making `truncatedSummary` a mutable string (call `mutableCopy` on it), make your current loop go in reverse, and pay attention to the character that you are replacing. – Sergey Kalinichenko Nov 13 '13 at 20:28
  • @dasblinkenlight Right, but then even if I write it up myself, I don't really want to accept my own answer. But I'd prefer to accept an answer that provides a complete answer to the question. Short of editing the code into your answer, I'm not sure how to proceed. – funroll Nov 13 '13 at 21:25
2

I tend towards @dasblinkenlight, it's just an algorithm after all, but here's some code. Few modern shorthands - used an old compiler. ARC assumed. Won't claim it's efficient, or beautiful, but it does work and handles edge cases (repeated colons, too many fields for limit):

- (NSString *)abbreviate:(NSString *)input limit:(NSUInteger)limit
{
    NSMutableArray *fields = [[input componentsSeparatedByString:@":"] mutableCopy];
    NSUInteger colonCount = fields.count - 1;

    if (colonCount >= limit)
        return [@"" stringByPaddingToLength:limit withString:@":" startingAtIndex:0];

    NSUInteger nonColonsRemaining = limit - colonCount;
    for (NSUInteger ix = 0; ix <= colonCount; ix++)
    {
        if (nonColonsRemaining > 0)
        {
            NSString *fieldValue = [fields objectAtIndex:ix];
            NSUInteger fieldLength = fieldValue.length;
            if (fieldLength <= nonColonsRemaining)
                nonColonsRemaining -= fieldLength;
            else
            {
                [fields replaceObjectAtIndex:ix withObject:[fieldValue substringToIndex:nonColonsRemaining]];
                nonColonsRemaining = 0;
            }
        }
        else
            [fields replaceObjectAtIndex:ix withObject:@""];
    }

    return [fields componentsJoinedByString:@":"];  
}
CRD
  • 52,522
  • 5
  • 70
  • 86
  • I understand your point about it just being an algorithm. The reason I wanted code was two-fold: 1. I needed this for a commit, 2. I find answers to SO questions most useful when they have working code, so I'd want to give preference to such answers with an accept. Thanks for this, I didn't catch the too-many-delimiters issue. – funroll Nov 13 '13 at 23:19