0

I'm working on a Delphi mobile app and want to convert camelCase or PascalCase identifiers into space-separated, human-readable labels.

For example this is my array:

const
  ReadableIdentifiers: array[0..8] of string = (
    'LoadUserData',
    'SaveToFile',
    'CreateNewUserAccount',
    'IsUserLoggedIn',
    'GenerateReportPreview',
    'HandleRequestError',
    'FetchUserProfile',
    'ParseDateString',
    'ValidateInputField'
  );

I want them to become:

Load User Data
Save To File
Create New User Account
Is User Logged In
Generate Report Preview
Handle Request Error
Fetch User Profile
Parse Date String
Validate Input Field

The goal is to add a space wherever a lowercase letter is immediately followed by an uppercase one as that is when a new word will begin in my array.

This is what I have done currently:

function ManualCamelCaseLabel(Input: string): string;
begin
  if Input = 'LoadUserData' then
    Result := 'Load User Data'
  else if Input = 'SaveToFile' then
    Result := 'Save To File'
  else if Input = 'CreateNewUserAccount' then
    Result := 'Create New User Account'
  else if Input = 'IsUserLoggedIn' then
    Result := 'Is User Logged In'
  else if Input = 'GenerateReportPreview' then
    Result := 'Generate Report Preview'
  else if Input = 'HandleRequestError' then
    Result := 'Handle Request Error'
  else if Input = 'FetchUserProfile' then
    Result := 'Fetch User Profile'
  else if Input = 'ParseDateString' then
    Result := 'Parse Date String'
  else if Input = 'ValidateInputField' then
    Result := 'Validate Input Field'
  else
    Result := Input;
end;

It works, but it's hardcoded and I need something better.

7 Answers 7

4

This solution uses an inverted for loop and the case statement, which drastically reduces the code to be written:

  • we start at the end
  • we step back to only the 2nd character (you don't want to put a space in front of the first character)
  • only ASCII texts are supported (latin letters A to Z) yet
  • however, it would also add spaces between every letter in an acronym, f.e. CERT would become C E R T
procedure InsertSpaces( var sText: String );
var
  iPos: Integer;
begin
  for iPos:= Length( sText ) downto 2 do begin  // From back to front.
    case sText[iPos] of  // Character at current position.
      'A'.. 'Z': Insert( ' ', sText, iPos );  // Put space in front of current character.
    end;
  end;
end;
Sign up to request clarification or add additional context in comments.

Comments

1

You can use Regex (Regular Expressions), specifically theTRegEx.Replace function from System.RegularExpressions.

You can make a little helper function like this:

function SplitCamelCase(const Input: string): string;
begin
  Result := TRegEx.Replace(Input, '([a-z])([A-Z])', '$1 $2');
end;

That'll do what you need with your example words.

Comments

1

There's also a different option that you can use if you prefer not to use RegEx.

You can use a standard For loop like this helper function:

function SplitCamelCase(const Input: string): string;
begin
  Result := '';
  for var I := 1 to Length(Input) do
  begin
    if (I > 1) and (Input[I] in ['A'..'Z']) and (Input[I - 1] in ['a'..'z']) then
      Result := Result + ' ';
    Result := Result + Input[I];
  end;
end;

5 Comments

To make it work even for non-ASCII strings one can use TCharacter.IsUpper(Input, I) and TCharacter.IsLower(Input, I-1).
Why starting the loop at 1 and permanently checking if it's > 1? Where's the common sense?
If (I > 1) wasn't there, then I would get a range check error with Input[I - 1] in the first iteration of the For loop. So I added (I > 1) to avoid getting that error.
That's because you insist on inline creating i with the loop, instead of making it available for the whole function (before begin).
I just prefer inline variables because of the improved readability of code. It's easier/faster to read/understand the code when using inline variables in my opinion. There's also the benefit of not having the need to always declare the type of the variable.
1

I like AmigoJack's answer, but I prefer a function over a procedure.

So here's a function version of his answer:

function SplitCamelCase(const Input: string): string;
begin
  Result := Input;
  for var I := Length(Result) downto 2 do
  begin
    case Result[I] of
      'A'..'Z': Insert(' ', Result, I);
    end;
  end;
end;

Comments

0

For the fun of it, here's another method to achieve this using a helper function that recursively calls itself until it's finished going through the word and adding spaces:

function SplitCamelCase(const Input: string; Index: UInt64 = 1): string;
begin
  if Index > Length(Input) then
    Exit('');
  if (Index > 1) and (Input[Index] in ['A'..'Z']) and (Input[Index - 1] in ['a'..'z']) then
    Result := ' ' + Input[Index]
  else
    Result := Input[Index];
  Result := Result + SplitCamelCase(Input, Index + 1);
end;

Comments

-1

Here's a function to help you do it:

function AddSpacesBetweenCamelCase(TheWord: string): string;
var
  P: PChar;
  Prev: Char;
begin
  Result := '';
  P := PChar(TheWord);
  Prev := #0;
  while P^ <> #0 do
  begin
    if (Prev in ['a'..'z']) and (P^ in ['A'..'Z']) then
      Result := Result + ' ';
    Result := Result + P^;
    Prev := P^;
    Inc(P);
  end;
end;

4 Comments

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. Would you kindly edit your answer to include additional details for the benefit of the community?
Why the speed optimization (using PChar) only on the input, but not on the output (String + String)? Why no further explanation?
@liansa111 Is there any benefit for doing it this way?
-1

Here's a function to help you do it using a temporary array of char:

function AddSpacesBetweenCamelCase(TheWord: string): string;
var
  Temp: array of Char;
  I, J: Integer;
begin
  SetLength(Temp, Length(TheWord) * 2);
  J := 0;
  for I := 1 to Length(TheWord) do
  begin
    if (I > 1) and (TheWord[I] in ['A'..'Z']) and (TheWord[I - 1] in ['a'..'z']) then
    begin
      Temp[J] := ' ';
      Inc(J);
    end;
    Temp[J] := TheWord[I];
    Inc(J);
  end;
  SetLength(Result, J);
  for I := 0 to J - 1 do
    Result[I + 1] := Temp[I];
end;

7 Comments

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. Would you kindly edit your answer to include additional details for the benefit of the community?
Why starting the loop at 1 and permanently checking if it's > 1? Where's the common sense?
I'm guessing his reason for (I > 1) is the same reason as mine. You need (I > 1) in order to avoid the range check error on TheWord[I - 1].
Your guessing is wrong and you forgot how to declare variables the proper way. Don't just dodge error messages - understand them. Why don't you try to compile this very code with my suggested modification?
@liansa111 Is there any benefit for doing it this way using var Temp: array of Char;?
I just ran a benchmark. Using var Temp: array of Char; instead of direct string manipulation is quite a bit slower.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.