2

I got asked a question and now I am kicking myself for not being able to come up with the exact/correct result.

Imagine we have a function that splits a string into multiple lines but each line has to have x number of characters before we "split" to the new line:

private string[] GetPagedMessages(string input, int maxCharsPerLine) { ... }

For each line, we need to incorporate, at the end of the line "x/y" which is basically 1/4, 2/4 etc... Now, the paging mechanism must also be part of the length restriction per line.

I have been overworked and overthinking and tripping up on things and this seems pretty straight forward but for the life of me, I cannot figure it out! What am I not "getting"?

What am I interested in? The calculation and some part of the logic but mainly the calculation of how many lines are required to split the input based on the max chars per line which also needs to include the x/y.

Remember: we can have more than a single digit for the x/y (i.e: not just 1/4 but also 10/17 or 99/200)

Samples:

input = "This is a long message"

maxCharsPerLine = 10

output:

This i 1/4 // << Max 10 chars
s a lo 2/4 // << Max 10 chars
ng mes 3/4 // << Max 10 chars
sage 4/4 // << Max 10 chars

Overall the logic is simple but its just the calculation that is throwing me off.

Ahmed ilyas
  • 5,722
  • 8
  • 44
  • 72
  • 2
    I guess you must first work out how many lines you have if you ignore the possible length of the x/y section. Based on this you can determine the length of the x/y portion, and then once again check if this changes the total number of lines. Repeat till you have a stable outcome. – DeMaki Jan 28 '21 at 09:56
  • Are you using a fixed-width font? If not, counting characters will not represent the actual display length of any string. – JonasH Jan 28 '21 at 10:58
  • I don't really understand it. Given that `input = "This is a long message"maxCharsPerLine = 10 should be split to 4 lines` and another `input = "123456", maxCharsPerLine = 6` Why the latter should be split into 3 lines and not just 1 line ? Where does this 3 lines come from ? It *could* be split to anything between 6 and 1 lines and still maxCharsPerLine <= 6 would be true – Fabjan Jan 28 '21 at 20:36
  • 1
    @Fabjan think of SMS messages. You want to fill the messages to the fullest within their limits to send as few as possible, but append a pager to let the receiver know there's more to come. So your message of 6 characters with a limit of 6, gets sent like "12 1/3", "34 2/3", "56 3/3". And yes, I have received SMS messages that were literally ". (3/3)", so it's not as trivial as it seems. – CodeCaster Jan 29 '21 at 10:29
  • 1
    @CodeCaster Thanks for the explanation. I missed the bit that pagination is also part of the message and limit of 6 chars includes it as well and smth like '1234561/1' would actually make it 9 chars long. – Fabjan Jan 29 '21 at 12:51

2 Answers2

2

The idea: First, find how many digits is the number of lines:

(n = input.Length, maxCharsPerLine = 10)
if n <= 9*(10-4)  ==> 1 digit
if n <= 9*(10-5) + 90*(10-6)  ==> 2 digits
if n <= 9*(10-6) + 90*(10-7) + 900*(10-8)  ==> 3 digits
if n <= 9*(10-7) + 90*(10-8) + 900*(10-9) + 9000*(10-10)  ==> No solution

Then, subtract the spare number of lines. The solution:

    private static int GetNumberOfLines(string input, int maxCharsPerLine)
    {
        int n = input.Length;
        int x = maxCharsPerLine;

        for (int i = 4; i < x; i++)
        {
            int j, sum = 0, d = 9, numberOfLines = 0;

            for (j = i; j <= i + i - 4; j++)
            {
                if (x - j <= 0)
                    return -1;   // No solution

                sum += d * (x - j);
                numberOfLines += d;
                d *= 10;
            }
            if (n <= sum)
                return numberOfLines - (sum - n) / (x - j + 1);
        }
        return -2;   // Invalid
    }

Usage:

    private static string[] GetPagedMessages(string input, int maxCharsPerLine)
    {
        int numberOfLines = GetNumberOfLines(input, maxCharsPerLine);
        if (numberOfLines < 0)
            return null;

        string[] result = new string[numberOfLines];

        int spaceLeftForLine = maxCharsPerLine - numberOfLines.ToString().Length - 2;  // Remove the chars of " x/y" except the incremental 'x'
        int inputPosition = 0;
        for (int line = 1; line < numberOfLines; line++)
        {
            int charsInLine = spaceLeftForLine - line.ToString().Length;
            result[line - 1] = input.Substring(inputPosition, charsInLine) + $" {line}/{numberOfLines}";
            inputPosition += charsInLine;
        }
        result[numberOfLines-1] = input.Substring(inputPosition) + $" {numberOfLines}/{numberOfLines}";
        return result;
    }
Udi Y
  • 258
  • 3
  • 12
  • 2
    I like the math, but I find the code unreadable. – CodeCaster Jan 28 '21 at 20:24
  • Agreed. Code is unreadable and uncommented. Why should i start from 4? Why is d = 9 to begin with? Why multiply by 10? I don't understand. – Ahmed ilyas Jan 29 '21 at 05:37
  • 1
    You're right. I concentrated on the algorithm rather than on readability. The main explanation is in my first paragraph. Look at the first line: `if n <= 9*(10-4) ==> 1 digit` The first lines have extra 4 characters in the end for: “ x/y”. This is why ‘i’ starts with 4. ‘d’ starts with 9 for the first 9 lines 1-9. Then *10 for 90 lines 10-99. Then *10 for 900 lines 100-999, etc. – Udi Y Jan 29 '21 at 09:48
  • Also, _"After you know the number of lines, it's quite easy to split to lines."_ - you'd have to repeat part of the calculation when splitting the input again, having to keep track of line length and pager size again. – CodeCaster Jan 29 '21 at 10:32
  • @CodeCaster Not necessarily. Look at my edit for usage. – Udi Y Jan 30 '21 at 09:40
1

A naive approach is to start counting the line lengths minus the "pager"'s size, until the line count changes in size ("1/9" is shorter than "1/10", which is shorter than "11/20", and so on):

private static int[] GetLineLengths(string input, int maxCharsPerLine)
{
    /* The "pager" (x/y) is at least 4 characters (including the preceding space) and at most ... 8?
    * 7/9     (4)
    * 1/10    (5)
    * 42/69   (6)
    * 3/123   (6)
    * 42/420  (7)
    * 999/999 (8)
    */

    int charsRemaining = input.Length;

    var lineLengths = new List<int>();

    // Start with " 1/2", (1 + 1 + 2) = 4 length
    var highestLineNumberLength = 1;

    var lineNumber = 0;
    do
    {
        lineNumber++;

        var currentLineNumberLength = lineNumber.ToString().Length; // 1 = 1, 99 = 2, ...
        if (currentLineNumberLength > highestLineNumberLength)
        {
            // Pager size changed, reset
            highestLineNumberLength = currentLineNumberLength;
            lineLengths.Clear();
            lineNumber = 0;
            charsRemaining = input.Length;
            continue;
        }

        var pagerSize = currentLineNumberLength + highestLineNumberLength + 2;
        var lineLength = maxCharsPerLine - pagerSize;

        if (lineLength <= 0)
        {
            throw new ArgumentException($"Can't split input of size {input.Length} into chunks of size {maxCharsPerLine}");
        }

        lineLengths.Add(lineLength);

        charsRemaining -= lineLength;

    }
    while (charsRemaining > 0);

    return lineLengths.ToArray();
}

Usage:

private static string[] GetPagedMessages(string input, int maxCharsPerLine)
{
    if (input.Length <= maxCharsPerLine)
    {
        // Assumption: no pager required for a message that takes one line
        return new[] { input };
    }

    var lineLengths = GetLineLengths(input, maxCharsPerLine);

    var result = new string[lineLengths.Length];

    // Cut the input and append the pager
    var previousIndex = 0;
    for (var i = 0; i < lineLengths.Length; i++)
    {
        var lineLength = Math.Min(lineLengths[i], input.Length - previousIndex); // To cater for final line being shorter
        result[i] = input.Substring(previousIndex, lineLength) + " " + (i + 1) + "/" + lineLengths.Length;
        previousIndex += lineLength;
    }

    return result;
}

Prints, for example:

This  1/20
is a  2/20
long  3/20
strin 4/20
g tha 5/20
t wil 6/20
l spa 7/20
n mor 8/20
e tha 9/20
n te 10/20
n li 11/20
nes  12/20
beca 13/20
use  14/20
of i 15/20
ts e 16/20
norm 17/20
ous  18/20
leng 19/20
th 20/20
CodeCaster
  • 147,647
  • 23
  • 218
  • 272
  • When the whole input enters into one line, it doesn't print " 1/1" in the end, and therefore doesn't split correctly. For example: input = "123456", maxCharsPerLine = 6 should be split into 3 lines, not one. Also, when no solution exists, it gets into an infinite loop. – Udi Y Jan 28 '21 at 18:27
  • The first case is commented on in code, the latter I thought of after posting but had no time for to fix yet. – CodeCaster Jan 28 '21 at 18:42