Centering a line of text

I need a procedure that "centers" a string inside a string that is consecutive characters of a fixed length. For instance,

======================== Instructions ============================


or

**************************** hehe ********************************


might be printed by calling

PrintBetweenChars("Instructions",'=');


or

PrintBetweenChars("hehe",'*');


respectively. Per my rules, the output will always have atleast one leading character and space and one trailing character and space, and the input will be shrunk if need be. So if input is abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ and the maximum line length is only 26 and the character is '-' then the ouput is

- abcdefghijklmnopqrstuv -


Furthermore, to handle the case of input having an odd length like 5, there will be one more character on the left than right. So the output would be like

-- abcdefghijklmnopqrstu -


if the input was abcdefghijklmnopqrstu and the maximum line length was 26. I tried implementing this and came up with the messiest-looking procedure ever:

public static void PrintBetweenChars ( string input, char c )
{

// Require input be at most (_outputMaxLength - 4) chars to accomodate 1 beginning and trailing char and space
// Shrink input if necessary
int shrunkInputLen = OutputFormatter._outputMaxLength - 4;
if ( input.Length > (shrunkInputLen) )
{
input.Remove(shrunkInputLen);
}
string charline = new String(c, (OutputFormatter._outputMaxLength - input.Length - 2) / 2);
Console.WriteLine("{0} {1} {2}", (input.Length & 1) == 1 ? charline + c.ToString() : charline, input, charline);
}


Is there a more compact, elegant, efficient, clever and readable way of doing this using the .NET library?

• I don't see OutputFormatter defined anywhere...am I missing something? Where else are you getting max length? – rory.ap Nov 11 '15 at 15:44
• "more compact, elegant, efficient, clever and readable?" All at the same time? Which of these are most important to you? – Mast Nov 11 '15 at 15:52
• "compact" should not be a goal - there is no reason to minify c# code unless it makes it more efficient. Get something that works (which it looks like you have) and that you understand ("readable"), then look for ways to make it better, ideally without sacrificing readability (either intrinsically or with comments). – D Stanley Nov 11 '15 at 15:55
• I added a compacted one liner to the solution which proves @DStanley point that it should not be the goal. – John Paul Nov 11 '15 at 16:29
• @BenAaronson: First, doing so makes it impossible during debugging to see what the actual input parameter was. That can lead to some confusion. Second, when deferred execution is involved it can cause unexpected behavior. See stackoverflow.com/questions/31993443/… for an extreme example. As far as I'm concerned, input-only parameters should be considered read-only (i.e. const). – Jim Mischel Nov 11 '15 at 19:20

In testing your code I noticed a crash with text longer than the max. Not sure if you consider this simpler but it does handle the tails case.

public void PrintBetweenChars(string input, char c)
{
// Require input be at most (_outputMaxLength - 4) chars to accomodate 1 beginning and trailing char and space
// Shrink input if necessary
string UserText = " " + input.Substring(0, Math.Min(OutputFormatter._outputMaxLength - 4, input.Length)) + " ";
string DisplayText = UserText.PadLeft((OutputFormatter._outputMaxLength / 2) + (UserText.Length / 2) + (UserText.Length % 2), c);
}

• Hi! Welcome to Code Review. Good job on your first post! – TheCoffeeCup Nov 11 '15 at 18:24

Bug

One day I'll have to stop doing this but... Unicode.

From what I understand of your function you always want a line to be the maximum number of characters. It won't.

Here's an example using your original code modified to use a max length of 64 rather the OutputFormatter stuff:

string e = "e";
string eAcute = "é".Normalize(NormalizationForm.FormD);
PrintBetweenChars(e, '*');
PrintBetweenChars(eAcute, '*');

// ******************************* e ******************************
// ****************************** é ******************************


The second output has 1 fewer 'character' in it.

Using NormalizationForm.FormD decomposes the string into its canonical form. I could have written var eAcute = "e\u0301"; but I would have had to look up the modifier character (which, I've ended up doing anyway).

There's no built-in way to do that in .NET, but if what worries you is the readability of your code, try some refactoring. Extract constants, use meaningful names, and pay attention to indenting. Here's an example:

public void PrintCenteredAndSurrounded(string input, char surroundingChar)
{
const int minimumSurroundingCharsOnEachSide = 1;
const int surroundingSpaceLength = 1;

int maxLengthToPrint =
OutputFormatter._outputMaxLength
- (minimumSurroundingCharsOnEachSide + surroundingSpaceLength) * 2;
if(input.Length > maxLengthToPrint)
{
input = input.Substring(0, maxLengthToPrint);
}

var surroundingCharlineLength =
(OutputFormatter._outputMaxLength
- input.Length
- minimumSurroundingCharsOnEachSide
- surroundingSpaceLength) / 2;

string charline = new String(surroundingChar, surroundingCharlineLength);

var extraCharNeeded = (input.Length & 1) == 1;
Console.WriteLine(
"{0}{1} {2} {3}",
extraCharNeeded ? surroundingChar.ToString() : "",
charline,
input,
charline);
}


The code is now lengthier and perhaps too verbose, but you get the idea.

This is what I came up with...

public static string PrintBetweenChars(string value, char c, int maxLength = 26)
{
var maxValueLength = maxLength - 2;

if (value.Length > maxValueLength)
{
value = value.Remove(maxValueLength);
}

var paddingLength = maxLength - value.Length;
var right = new string(c, halfPaddingLength);

return \$"{left} {value} {right}";
}


The important things to note:

• I've made it a pure function, there's no dependency on the OutputFormatter or the Console. The function should probably be renamed at this point.
• maxLength is passed in, but it has a default value
• I tried to name the variables so that the code is self documenting.
• The last line uses a C# 6 feature called string interpolation. It can be replaced by a string.Format call if needed.

To use it you might do something like:

var value = PrintBetweenChars("hello world", '=', OutputFormatter._outputMaxLength);
Console.WriteLine(value);


Making it a pure function has many benefits. It could be pulled out into a library or perhaps turned into a string extension method.

Would it be weird to go?

Console.WriteLine("hello world".BetweenChars('=', 15));


Anyway, that was a fun one :)

• string has a constructor that allows you to specify a repeating char so you don't need the string.Concat(Enumerable.Repeat( stuff, new string(char, int) will do it. – RobH Nov 13 '15 at 12:05
• @RobH good call. I've made the change. – craftworkgames Nov 13 '15 at 22:13

Is there a more compact, elegant, efficient, clever and readable way of doing this using the .NET library?

There's no built-in for something so specific, but it's just a case of figuring out the logic of what you need and writing it out.

This is more compact, and I think more elegant, clever and (perhaps) readable. I don't know about efficient, you'd have to test it.

private string FormatString(char c, string input, int maxLength)
{
// The maximum length the text can be is four less than the whole maxLength, to allow for the space and character requirement either side
int maxTextLength = maxLength - 4;

// The text to display is therefore a substring of the original text, if the maxtextlength is shorter than the input length
input = input.Substring(0, Math.Min(maxTextLength, input.Length));

// surround the final text with spaces and one character
input = c + " " + input + " " + c;

// The number of characters to append now is the difference, but at least 0. Then half it (integer division), because they need to be the same either side
int charCount = Math.Max((maxLength - input.Length) / 2, 0);

// put the same number of the char either side, and padleft to add the additional one if required
return string.Format("{0}{1}{0}", new string(c, charCount), input).PadLeft(maxLength, c);
}


That said, words like "elegant", "clever" and "readable" are fairly subjective terms.

This should work, uses padding and inserting which I think is easier to read.

static void Main(string[] args)
{
Console.WriteLine(PrintBetweenChars("abcde", 10, '-'));
Console.WriteLine(PrintBetweenChars("abcdef", 10, '-'));
Console.Write(PrintBetweenChars("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 26, '*'));
}

static string PrintBetweenChars(string input, int maxLength, char c)
{
input = input.Insert(input.Length, " ").Insert(0, " ");

if (input.Length > maxLength)
return input.Substring(0, maxLength - 3).Insert(maxLength - 3, " ").PadLeft(maxLength - 1, c).PadRight(maxLength, c);
else
return input.PadLeft(input.Length + ((maxLength - input.Length)/2) + (((maxLength - input.Length) % 2 != 1) ? 0 : 1), c)
.PadRight(input.Length + (maxLength - input.Length) , c);
}


EDIT

If you really want you could make it a one liner:

static string PrintBetweenChars(string input, int maxLength, char c)
{
return input.Length > maxLength ? input.Insert(input.Length, " ").Insert(0, " ").Substring(0, maxLength - 3).Insert(maxLength - 3, " ").PadLeft(maxLength - 1, c).PadRight(maxLength, c)
: input.Insert(input.Length, " ").Insert(0, " ").PadLeft(input.Length + ((maxLength - input.Length) / 2) + (((maxLength - input.Length) % 2 != 1) ? 0 : 1), c)
.PadRight(input.Length + (maxLength - input.Length), c);
}