.NET - How can you split a "caps" delimited string into an array?
Asked Answered
D

19

123

How do I go from this string: "ThisIsMyCapsDelimitedString"

...to this string: "This Is My Caps Delimited String"

Fewest lines of code in VB.net is preferred but C# is also welcome.

Cheers!

Donela answered 30/9, 2008 at 22:3 Comment(3)
What happens when you have to deal with "OldMacDonaldAndMrO'TooleWentToMcDonalds"?Factual
It's only going to see limited use. I'll mainly just be using it to parse variable names such as ThisIsMySpecialVariable,Donela
This worked for me: Regex.Replace(s, "([A-Z0-9]+)", " $1").Trim(). And if you want to split on each capital letter, just remove the plus.Electroencephalogram
C
188

I made this a while ago. It matches each component of a CamelCase name.

/([A-Z]+(?=$|[A-Z][a-z])|[A-Z]?[a-z]+)/g

For example:

"SimpleHTTPServer" => ["Simple", "HTTP", "Server"]
"camelCase" => ["camel", "Case"]

To convert that to just insert spaces between the words:

Regex.Replace(s, "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1 ")

If you need to handle digits:

/([A-Z]+(?=$|[A-Z][a-z]|[0-9])|[A-Z]?[a-z]+|[0-9]+)/g

Regex.Replace(s,"([a-z](?=[A-Z]|[0-9])|[A-Z](?=[A-Z][a-z]|[0-9])|[0-9](?=[^0-9]))","$1 ")
Calebcaledonia answered 30/9, 2008 at 22:59 Comment(7)
CamelCase! That's what it was called! I love it! Thanks much!Donela
Actually camelCase has a leading lowercase letter. What you're referring to here is PascalCase.Forestry
...and when you refer to something that can be "camel case" or "pascal case" it is called "intercapped"Dehart
Doesn't split "Take5" which would fails my use caseBookmaker
@Bookmaker Digits was not in the question, so my answer did not account for them. I've added a variant of the patterns that accounts for digits.Calebcaledonia
i have to correct bad info. CamelCase does not have to start with a lower-case letter. It can start with either. PascalCase is literally, by definition, Upper CamelCase.Pneumonoultramicroscopicsilicovolcanoconiosis
I would change the last part from [0-9](?=[^0-9]) to [0-9](?=[A-Za-z]) so that if you have a space after a number it won't double the space. (Sure, you're not supposed to have a space in a camel case string, but that makes it more general and accept complete sentences which contain camel case expressions).Railroad
A
42
Regex.Replace("ThisIsMyCapsDelimitedString", "(\\B[A-Z])", " $1")
Amniocentesis answered 30/9, 2008 at 22:14 Comment(8)
This is the best solution so far, but you need to use \\B to compile. Otherwise the compiler tries to treat the \B as an escape sequence.Kenwrick
Nice solution. Can anyone think of a reason that this shouldn't be the accepted answer? Is it less capable or less performant?Forestry
This one treats consecutive caps as separate words (e.g. ANZAC is 5 words) where as MizardX's answer treats it (correctly IMHO) as one word.Lustrate
@Ray, I'd argue that "ANZAC" should be written as "Anzac" to be considered a pascal case word since it's not English-case.Rhyme
@Rhyme ANZAC is an Acronym for Australia (and) New Zealand Army Corps so should be in all caps.Sophistry
@Neaox, in English it should be, but this isn't acronym-case or normal-english-case; it's caps-delimited. If the source text should be capitalised the same way that it is in normal English, then other letters shouldn't be capitalised either. For example, why should the "i" in "is" be capitalised to fit the caps-delimited format but not the "NZAC" in "ANZAC"? Strictly speaking, if you interpret "ANZAC" as caps-delimited then it is 5 words, one for each letter.Rhyme
This still fails on "Take5" and converts "Arg20" to "arg 20" - fails 3 out of my 4 testsBookmaker
@Sophistry The official capitalization guidelines for C# say that even acronyms (and other initialisms) should be lower case after the first letter. It gives the example of HtmlTag, even though HTML would be upper case in every situation ANZAC would be. However, it does make an exception for two-level acronyms, giving IOStream as an example (for IO Stream).Negotiant
S
22

Great answer, MizardX! I tweaked it slightly to treat numerals as separate words, so that "AddressLine1" would become "Address Line 1" instead of "Address Line1":

Regex.Replace(s, "([a-z](?=[A-Z0-9])|[A-Z](?=[A-Z][a-z]))", "$1 ")
Schaeffer answered 15/11, 2008 at 0:31 Comment(3)
Great addition! I suspect not a few people will be surprised by the accepted answer's handling of numbers in strings. :)Watkins
I know it's been almost 8 years since you posted this, but it worked perfectly for me, too. :) The numbers tripped me up at first.Unessential
The only answer that passes my 2 outlier tests: "Take5" -> "Take 5", "PublisherID" -> "Publisher ID". I want to upvote this twiceBookmaker
I
18

Just for a little variety... Here's an extension method that doesn't use a regex.

public static class CamelSpaceExtensions
{
    public static string SpaceCamelCase(this String input)
    {
        return new string(Enumerable.Concat(
            input.Take(1), // No space before initial cap
            InsertSpacesBeforeCaps(input.Skip(1))
        ).ToArray());
    }

    private static IEnumerable<char> InsertSpacesBeforeCaps(IEnumerable<char> input)
    {
        foreach (char c in input)
        {
            if (char.IsUpper(c)) 
            { 
                yield return ' '; 
            }

            yield return c;
        }
    }
}
Inshrine answered 30/9, 2008 at 22:59 Comment(4)
To avoid using Trim(), before the foreach I put: int counter = -1. inside, add counter++. change the check to: if (char.IsUpper(c) && counter > 0)Selfgratification
This inserts a space before the 1st char.Paroxysm
I've taken the liberty of fixing the issue pointed out by @ZarShardan. Please feel free to roll back or edit to your own fix if you dislike the change.Ranaerancagua
Can this be enhanced to handle abbreviations for example by adding a space before the last uppercase in a series of uppercase letters e.g BOEForecast => BOE ForecastDisobey
G
12

Grant Wagner's excellent comment aside:

Dim s As String = RegularExpressions.Regex.Replace("ThisIsMyCapsDelimitedString", "([A-Z])", " $1")
Goering answered 30/9, 2008 at 22:13 Comment(1)
Good point... Please feel free to insert the .substring(), .trimstart(), .trim(), .remove(), etc. of your choice. :)Goering
J
11

I needed a solution that supports acronyms and numbers. This Regex-based solution treats the following patterns as individual "words":

  • A capital letter followed by lowercase letters
  • A sequence of consecutive numbers
  • Consecutive capital letters (interpreted as acronyms) - a new word can begin using the last capital, e.g. HTMLGuide => "HTML Guide", "TheATeam" => "The A Team"

You could do it as a one-liner:

Regex.Replace(value, @"(?<!^)((?<!\d)\d|(?(?<=[A-Z])[A-Z](?=[a-z])|[A-Z]))", " $1")

A more readable approach might be better:

using System.Text.RegularExpressions;

namespace Demo
{
    public class IntercappedStringHelper
    {
        private static readonly Regex SeparatorRegex;

        static IntercappedStringHelper()
        {
            const string pattern = @"
                (?<!^) # Not start
                (
                    # Digit, not preceded by another digit
                    (?<!\d)\d 
                    |
                    # Upper-case letter, followed by lower-case letter if
                    # preceded by another upper-case letter, e.g. 'G' in HTMLGuide
                    (?(?<=[A-Z])[A-Z](?=[a-z])|[A-Z])
                )";

            var options = RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled;

            SeparatorRegex = new Regex(pattern, options);
        }

        public static string SeparateWords(string value, string separator = " ")
        {
            return SeparatorRegex.Replace(value, separator + "$1");
        }
    }
}

Here's an extract from the (XUnit) tests:

[Theory]
[InlineData("PurchaseOrders", "Purchase-Orders")]
[InlineData("purchaseOrders", "purchase-Orders")]
[InlineData("2Unlimited", "2-Unlimited")]
[InlineData("The2Unlimited", "The-2-Unlimited")]
[InlineData("Unlimited2", "Unlimited-2")]
[InlineData("222Unlimited", "222-Unlimited")]
[InlineData("The222Unlimited", "The-222-Unlimited")]
[InlineData("Unlimited222", "Unlimited-222")]
[InlineData("ATeam", "A-Team")]
[InlineData("TheATeam", "The-A-Team")]
[InlineData("TeamA", "Team-A")]
[InlineData("HTMLGuide", "HTML-Guide")]
[InlineData("TheHTMLGuide", "The-HTML-Guide")]
[InlineData("TheGuideToHTML", "The-Guide-To-HTML")]
[InlineData("HTMLGuide5", "HTML-Guide-5")]
[InlineData("TheHTML5Guide", "The-HTML-5-Guide")]
[InlineData("TheGuideToHTML5", "The-Guide-To-HTML-5")]
[InlineData("TheUKAllStars", "The-UK-All-Stars")]
[InlineData("AllStarsUK", "All-Stars-UK")]
[InlineData("UKAllStars", "UK-All-Stars")]
Jitter answered 11/11, 2014 at 22:50 Comment(1)
+ 1 for explaining the regex and making it this readable. And I learned something new. There is a free-spacing mode and comments in .NET Regex. Thank you!Abode
I
4

For more variety, using plain old C# objects, the following produces the same output as @MizardX's excellent regular expression.

public string FromCamelCase(string camel)
{   // omitted checking camel for null
    StringBuilder sb = new StringBuilder();
    int upperCaseRun = 0;
    foreach (char c in camel)
    {   // append a space only if we're not at the start
        // and we're not already in an all caps string.
        if (char.IsUpper(c))
        {
            if (upperCaseRun == 0 && sb.Length != 0)
            {
                sb.Append(' ');
            }
            upperCaseRun++;
        }
        else if( char.IsLower(c) )
        {
            if (upperCaseRun > 1) //The first new word will also be capitalized.
            {
                sb.Insert(sb.Length - 1, ' ');
            }
            upperCaseRun = 0;
        }
        else
        {
            upperCaseRun = 0;
        }
        sb.Append(c);
    }

    return sb.ToString();
}
Imperceptive answered 1/10, 2008 at 2:44 Comment(1)
Wow, that's ugly. Now I remember why I so dearly love regex! +1 for effort, though. ;)Juno
T
3

Below is a prototype that converts the following to Title Case:

  • snake_case
  • camelCase
  • PascalCase
  • sentence case
  • Title Case (keep current formatting)

Obviously you would only need the "ToTitleCase" method yourself.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var examples = new List<string> { 
            "THEQuickBrownFox",
            "theQUICKBrownFox",
            "TheQuickBrownFOX",
            "TheQuickBrownFox",
            "the_quick_brown_fox",
            "theFOX",
            "FOX",
            "QUICK"
        };

        foreach (var example in examples)
        {
            Console.WriteLine(ToTitleCase(example));
        }
    }

    private static string ToTitleCase(string example)
    {
        var fromSnakeCase = example.Replace("_", " ");
        var lowerToUpper = Regex.Replace(fromSnakeCase, @"(\p{Ll})(\p{Lu})", "$1 $2");
        var sentenceCase = Regex.Replace(lowerToUpper, @"(\p{Lu}+)(\p{Lu}\p{Ll})", "$1 $2");
        return new CultureInfo("en-US", false).TextInfo.ToTitleCase(sentenceCase);
    }
}

The console out would be as follows:

THE Quick Brown Fox
The QUICK Brown Fox
The Quick Brown FOX
The Quick Brown Fox
The Quick Brown Fox
The FOX
FOX
QUICK

Blog Post Referenced

Toting answered 3/8, 2015 at 20:27 Comment(0)
K
2
string s = "ThisIsMyCapsDelimitedString";
string t = Regex.Replace(s, "([A-Z])", " $1").Substring(1);
Kenwrick answered 30/9, 2008 at 22:14 Comment(4)
I knew there would be an easy RegEx way... I've got to start using it more.Settlings
Not a regex guru but what happens with "HeresAWTFString"?Rascon
You get "Heres A W T F String" but that's exactly what Matias Nino asked for in the question.Settlings
Yeah he needs to add that "multiple adjacent capitals are left alone". Which is pretty obviously required in many cases eg "PublisherID" here goes to "Publisher I D" which is awfulBookmaker
P
2

Regex is about 10-12 times slower than a simple loop:

    public static string CamelCaseToSpaceSeparated(this string str)
    {
        if (string.IsNullOrEmpty(str))
        {
            return str;
        }

        var res = new StringBuilder();

        res.Append(str[0]);
        for (var i = 1; i < str.Length; i++)
        {
            if (char.IsUpper(str[i]))
            {
                res.Append(' ');
            }
            res.Append(str[i]);

        }
        return res.ToString();
    }
Paroxysm answered 4/10, 2017 at 14:28 Comment(0)
V
1

Naive regex solution. Will not handle O'Conner, and adds a space at the start of the string as well.

s = "ThisIsMyCapsDelimitedString"
split = Regex.Replace(s, "[A-Z0-9]", " $&");
Vagarious answered 30/9, 2008 at 22:18 Comment(2)
I modded you up, but people generally take a smackdown better if it doesn't start with "naive".Ackerley
I don't think that was a smackdown. In this context, naive usually means obvious or simple (i.e. not necessarily the best solution). There is no intention of insult.Kenwrick
K
1

For C# building on this awesome answer by @ZombieSheep but now using a compiled regex for better performance:

public static class StringExtensions
{
    private static readonly Regex _regex1 = new(@"(\P{Ll})(\P{Ll}\p{Ll})", RegexOptions.Compiled | RegexOptions.CultureInvariant);
    private static readonly Regex _regex2 = new(@"(\p{Ll})(\P{Ll})", RegexOptions.Compiled | RegexOptions.CultureInvariant);

    public static string SplitCamelCase(this string str)
    {
        return _regex2.Replace(_regex1.Replace(str, "$1 $2"), "$1 $2");
    }
}

Sample code:

private static void Main(string[] args)
{
    string str = "ThisIsAPropertyNAMEWithNumber10";

    Console.WriteLine(str.SplitCamelCase());
}

Result:

This Is A Property NAME With Number 10

A plus point of this one is that it also works for strings that contain digits/numbers.

Kinney answered 17/12, 2022 at 2:50 Comment(1)
I wouldn't be able to pull that answer together again now, but hearty upvote for the compiled regex.Forceful
S
0

There's probably a more elegant solution, but this is what I come up with off the top of my head:

string myString = "ThisIsMyCapsDelimitedString";

for (int i = 1; i < myString.Length; i++)
{
     if (myString[i].ToString().ToUpper() == myString[i].ToString())
     {
          myString = myString.Insert(i, " ");
          i++;
     }
}
Settlings answered 30/9, 2008 at 22:12 Comment(0)
S
0

Try to use

"([A-Z]*[^A-Z]*)"

The result will fit for alphabet mix with numbers

Regex.Replace("AbcDefGH123Weh", "([A-Z]*[^A-Z]*)", "$1 ");
Abc Def GH123 Weh  

Regex.Replace("camelCase", "([A-Z]*[^A-Z]*)", "$1 ");
camel Case  
Steib answered 25/8, 2014 at 6:4 Comment(0)
E
0

Implementing the psudo code from: https://mcmap.net/q/75517/-insert-spaces-between-words-on-a-camel-cased-token-duplicate

    private static StringBuilder camelCaseToRegular(string i_String)
    {
        StringBuilder output = new StringBuilder();
        int i = 0;
        foreach (char character in i_String)
        {
            if (character <= 'Z' && character >= 'A' && i > 0)
            {
                output.Append(" ");
            }
            output.Append(character);
            i++;
        }
        return output;
    }
Eccles answered 14/5, 2016 at 21:16 Comment(0)
E
0

To match between non-uppercase and Uppercase Letter Unicode Category : (?<=\P{Lu})(?=\p{Lu})

Dim s = Regex.Replace("CorrectHorseBatteryStaple", "(?<=\P{Lu})(?=\p{Lu})", " ")
Enactment answered 15/5, 2017 at 2:31 Comment(0)
V
0

Procedural and fast impl:

  /// <summary>
  /// Get the words in a code <paramref name="identifier"/>.
  /// </summary>
  /// <param name="identifier">The code <paramref name="identifier"/></param> to extract words from.
  public static string[] GetWords(this string identifier) {
     Contract.Ensures(Contract.Result<string[]>() != null, "returned array of string is not null but can be empty");
     if (identifier == null) { return new string[0]; }
     if (identifier.Length == 0) { return new string[0]; }

     const int MIN_WORD_LENGTH = 2;  //  Ignore one letter or one digit words

     var length = identifier.Length;
     var list = new List<string>(1 + length/2); // Set capacity, not possible more words since we discard one char words
     var sb = new StringBuilder();
     CharKind cKindCurrent = GetCharKind(identifier[0]); // length is not zero here
     CharKind cKindNext = length == 1 ? CharKind.End : GetCharKind(identifier[1]);

     for (var i = 0; i < length; i++) {
        var c = identifier[i];
        CharKind cKindNextNext = (i >= length - 2) ? CharKind.End : GetCharKind(identifier[i + 2]);

        // Process cKindCurrent
        switch (cKindCurrent) {
           case CharKind.Digit:
           case CharKind.LowerCaseLetter:
              sb.Append(c); // Append digit or lowerCaseLetter to sb
              if (cKindNext == CharKind.UpperCaseLetter) {
                 goto TURN_SB_INTO_WORD; // Finish word if next char is upper
              }
              goto CHAR_PROCESSED;
           case CharKind.Other:
              goto TURN_SB_INTO_WORD;
           default:  // charCurrent is never Start or End
              Debug.Assert(cKindCurrent == CharKind.UpperCaseLetter);
              break;
        }

        // Here cKindCurrent is UpperCaseLetter
        // Append UpperCaseLetter to sb anyway
        sb.Append(c); 

        switch (cKindNext) {
           default:
              goto CHAR_PROCESSED;

           case CharKind.UpperCaseLetter: 
              //  "SimpleHTTPServer"  when we are at 'P' we need to see that NextNext is 'e' to get the word!
              if (cKindNextNext == CharKind.LowerCaseLetter) {
                 goto TURN_SB_INTO_WORD;
              }
              goto CHAR_PROCESSED;

           case CharKind.End:
           case CharKind.Other:
              break; // goto TURN_SB_INTO_WORD;
        }

        //------------------------------------------------

     TURN_SB_INTO_WORD:
        string word = sb.ToString();
        sb.Length = 0;
        if (word.Length >= MIN_WORD_LENGTH) {  
           list.Add(word);
        }

     CHAR_PROCESSED:
        // Shift left for next iteration!
        cKindCurrent = cKindNext;
        cKindNext = cKindNextNext;
     }

     string lastWord = sb.ToString();
     if (lastWord.Length >= MIN_WORD_LENGTH) {
        list.Add(lastWord);
     }
     return list.ToArray();
  }
  private static CharKind GetCharKind(char c) {
     if (char.IsDigit(c)) { return CharKind.Digit; }
     if (char.IsLetter(c)) {
        if (char.IsUpper(c)) { return CharKind.UpperCaseLetter; }
        Debug.Assert(char.IsLower(c));
        return CharKind.LowerCaseLetter;
     }
     return CharKind.Other;
  }
  enum CharKind {
     End, // For end of string
     Digit,
     UpperCaseLetter,
     LowerCaseLetter,
     Other
  }

Tests:

  [TestCase((string)null, "")]
  [TestCase("", "")]

  // Ignore one letter or one digit words
  [TestCase("A", "")]
  [TestCase("4", "")]
  [TestCase("_", "")]
  [TestCase("Word_m_Field", "Word Field")]
  [TestCase("Word_4_Field", "Word Field")]

  [TestCase("a4", "a4")]
  [TestCase("ABC", "ABC")]
  [TestCase("abc", "abc")]
  [TestCase("AbCd", "Ab Cd")]
  [TestCase("AbcCde", "Abc Cde")]
  [TestCase("ABCCde", "ABC Cde")]

  [TestCase("Abc42Cde", "Abc42 Cde")]
  [TestCase("Abc42cde", "Abc42cde")]
  [TestCase("ABC42Cde", "ABC42 Cde")]
  [TestCase("42ABC", "42 ABC")]
  [TestCase("42abc", "42abc")]

  [TestCase("abc_cde", "abc cde")]
  [TestCase("Abc_Cde", "Abc Cde")]
  [TestCase("_Abc__Cde_", "Abc Cde")]
  [TestCase("ABC_CDE_FGH", "ABC CDE FGH")]
  [TestCase("ABC CDE FGH", "ABC CDE FGH")] // Should not happend (white char) anything that is not a letter/digit/'_' is considered as a separator
  [TestCase("ABC,CDE;FGH", "ABC CDE FGH")] // Should not happend (,;) anything that is not a letter/digit/'_' is considered as a separator
  [TestCase("abc<cde", "abc cde")]
  [TestCase("abc<>cde", "abc cde")]
  [TestCase("abc<D>cde", "abc cde")]  // Ignore one letter or one digit words
  [TestCase("abc<Da>cde", "abc Da cde")]
  [TestCase("abc<cde>", "abc cde")]

  [TestCase("SimpleHTTPServer", "Simple HTTP Server")]
  [TestCase("SimpleHTTPS2erver", "Simple HTTPS2erver")]
  [TestCase("camelCase", "camel Case")]
  [TestCase("m_Field", "Field")]
  [TestCase("mm_Field", "mm Field")]
  public void Test_GetWords(string identifier, string expectedWordsStr) {
     var expectedWords = expectedWordsStr.Split(' ');
     if (identifier == null || identifier.Length <= 1) {
        expectedWords = new string[0];
     }

     var words = identifier.GetWords();
     Assert.IsTrue(words.SequenceEqual(expectedWords));
  }
Violence answered 10/1, 2018 at 10:49 Comment(0)
P
0

A simple solution, which should be order(s) of magnitude faster than a regex solution (based on the tests I ran against the top solutions in this thread), especially as the size of the input string grows:

string s1 = "ThisIsATestStringAbcDefGhiJklMnoPqrStuVwxYz";
string s2;
StringBuilder sb = new StringBuilder();

foreach (char c in s1)
    sb.Append(char.IsUpper(c)
        ? " " + c.ToString()
        : c.ToString());

s2 = sb.ToString();
Precarious answered 24/10, 2018 at 21:56 Comment(0)
R
0
Regex.Replace(str, @"(\p{Ll}(?=[\p{Lu}0-9])|\p{Lu}(?=\p{Lu}\p{Ll}|[0-9])|[0-9](?=\p{L}))", "$1 ")

It deals with all Unicode characters, plus it works fine if your string is a regular sentence that contains a camel case expression (and you want to keep the sentence intact but to break the camel case into words, without duplicating spaces etc).

I took Markus Jarderot's answer which is excellent (so credits to him) and replaced [A-Z] with \p{Lu} and [a-z] with \p{Ll} and modified the last part to deal with numbers.


If you want numbers to trail after acronyms (e.g. HTML5GuideHTML5 Guide):

Regex.Replace(str, @"(\p{Ll}(?=[\p{Lu}0-9])|\p{Lu}(?=\p{Lu}\p{Ll})|[0-9](?=\p{L}))", " $1")

Another approach

Just another approach to solve the problem:

Regex.Replace(str, @"((?<=[\p{Ll}0-9])\p{Lu}|(?<=\p{Lu})\p{Lu}(?=\p{Ll})|(?<=\p{L})[0-9]|(?<=[0-9])\p{Ll})", " $1")

More Options

If you want numbers to trail after acronyms (e.g. HTML5GuideHTML5 Guide):

Regex.Replace(str, @"((?<=[\p{Ll}0-9])\p{Lu}|(?<=\p{Lu})\p{Lu}(?=\p{Ll})|(?<=\p{Ll})[0-9]|(?<=[0-9])\p{Ll})", " $1")

If you want numbers to trail after any word (e.g. Html5GuideHtml5 Guide):

Regex.Replace(str, @"((?<=[\p{Ll}0-9])\p{Lu}|(?<=\p{Lu})\p{Lu}(?=\p{Ll})|(?<=[0-9])\p{Ll})", " $1")

If you don't want to deal with numbers and you're sure to not have them in the string:

Regex.Replace(str, @"((?<=\p{Ll})\p{Lu}|(?<=\p{Lu})\p{Lu}(?=\p{Ll}))", " $1")

For a simpler version (ignoring special Unicode characters like é as in fiancé),
pick any of the above regexes and simply
replace \p{Lu} with [A-Z], \p{Ll} with [a-z] and \p{L} with [A-Za-z].

Railroad answered 22/2, 2023 at 22:16 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.