Skip to content
This repository was archived by the owner on Aug 30, 2025. It is now read-only.

Commit 180122a

Browse files
authored
Merge pull request #14 from TechNobre/develop
Deploy nuget version 2.1.0 - Added extension `string.ToSnakeCase()`;
2 parents aadb13c + 0f9aa84 commit 180122a

10 files changed

Lines changed: 167 additions & 40 deletions

File tree

.github/workflows/publish-nuget.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ jobs:
1616
- uses: actions/checkout@main
1717

1818
- name: Setup .NET
19-
uses: actions/setup-dotnet@v1
19+
uses: actions/setup-dotnet@v2
2020
with:
21-
dotnet-version: '6.0.x'
21+
dotnet-version: '6.0.200'
2222

2323
- name: Restore dependencies
2424
run: dotnet restore

.github/workflows/sonarcloud.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,30 @@ jobs:
1818
steps:
1919

2020
- name: Setup .NET
21-
uses: actions/setup-dotnet@v1
21+
uses: actions/setup-dotnet@v2
2222
with:
23-
dotnet-version: '6.0.x'
23+
dotnet-version: '6.0.200'
2424

2525
- name: Set up JDK 11
26-
uses: actions/setup-java@v1
26+
uses: actions/setup-java@v3.0.0
2727
with:
28-
java-version: 1.11
28+
distribution: 'adopt'
29+
java-version: '11'
2930

3031
- uses: actions/checkout@v2
3132
with:
3233
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
3334

3435
- name: Cache SonarCloud packages
35-
uses: actions/cache@v1
36+
uses: actions/cache@v2.1.7
3637
with:
3738
path: ~\sonar\cache
3839
key: ${{ runner.os }}-sonar
3940
restore-keys: ${{ runner.os }}-sonar
4041

4142
- name: Cache SonarCloud scanner
4243
id: cache-sonar-scanner
43-
uses: actions/cache@v1
44+
uses: actions/cache@v2.1.7
4445
with:
4546
path: .\.sonar\scanner
4647
key: ${{ runner.os }}-sonar-scanner

.github/workflows/test-project.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ jobs:
2020
- uses: actions/checkout@v2
2121

2222
- name: Setup .NET
23-
uses: actions/setup-dotnet@v1
23+
uses: actions/setup-dotnet@v2
2424
with:
25-
dotnet-version: '6.0.x'
25+
dotnet-version: '6.0.200'
2626

2727
- name: Restore dependencies
2828
run: dotnet restore

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33

44

55

6+
## [2.1.0] - 2022-03-04
7+
[Full Changelog](https://github.com/TechNobre/PowerUtils.Text/compare/v2.0.0...v2.1.0)
8+
9+
10+
### New Features
11+
12+
- Added extension `string.ToSnakeCase()`;
13+
14+
15+
#### Enhancements
16+
17+
- Simplified code;
18+
19+
20+
21+
622
## [2.0.0] - 2022-02-10
723
[Full Changelog](https://github.com/TechNobre/PowerUtils.Text/compare/v1.2.0...v2.0.0)
824

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Helpers, extensions and utilities for manipulating
3131
- [CleanSpecialCharacters](#string.CleanSpecialCharacters)
3232
- [UppercaseFirst](#string.UppercaseFirst)
3333
- [LowercaseFirst](#string.LowercaseFirst)
34+
- [ToSnakeCase](#string.ToSnakeCase)
3435
- [NetworkExtensions](#NetworkExtensions)
3536
- [IsEmail](#string.IsEmail)
3637
- [CombineURL](#string.CombineURL)
@@ -142,6 +143,14 @@ Uppercase the first character
142143
var result = "Hello world!!!".UppercaseFirst();
143144
```
144145

146+
#### string.ToSnakeCase(); <a name="string.ToSnakeCase"></a>
147+
Convert a text to snake case format
148+
149+
```csharp
150+
// result = "test_snake_case"
151+
var result = "TestSnakeCase".ToSnakeCase();
152+
```
153+
145154

146155

147156
### TextExtensions <a name="TextExtensions"></a>

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "6.0.100"
3+
"version": "6.0.200"
44
}
55
}

src/PowerUtils.Text.csproj

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,18 @@
1414
<PackageId>PowerUtils.Text</PackageId>
1515
<title>PowerUtils.Text</title>
1616
<Product>PowerUtils.Text</Product>
17-
<Version>2.0.0</Version>
17+
<Version>2.1.0</Version>
1818

1919
<Authors>Nelson Nobre</Authors>
2020
<Company>TechNobre</Company>
2121

2222
<License>MIT</License>
2323
<PackageLicenseFile>LICENSE</PackageLicenseFile>
24-
<Copyright>Copyright © 2018 by TechNobre</Copyright>
24+
<Copyright>Copyright © 2022 by TechNobre</Copyright>
2525

2626
<Description>Helpers, extensions and utilities for manipulating strings</Description>
2727
<PackageReleaseNotes>
28-
Added extension `string.IsEmail()`;
29-
Added extension `string.CombineURL()`;
30-
Added extension `object.ToQueryString()`;
31-
Discontinued the extension `string.EmptyOrWhiteSpace()`. New method `string.EmptyOrWhiteSpaceToNull()`;
28+
Added extension `string.ToSnakeCase()`;
3229
</PackageReleaseNotes>
3330
<Summary>Helpers, extensions and utilities for manipulating strings</Summary>
3431
<PackageTags>PowerUtils;Utils;Helpers;Text;Formatting;Extension;Extensions;String</PackageTags>

src/TextExtensions.cs

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Globalization;
23
using System.Linq;
34
using System.Text;
45
using System.Text.RegularExpressions;
@@ -142,14 +143,9 @@ public static string Truncate(this string input, int maxLength)
142143
/// <returns>Capitalized name</returns>
143144
public static string CapitalizeName(this string input)
144145
{
145-
if(input == null)
146-
{
147-
return null;
148-
}
149-
150-
if(input == "")
146+
if(string.IsNullOrEmpty(input))
151147
{
152-
return "";
148+
return input;
153149
}
154150

155151
var splitedText = string
@@ -225,14 +221,9 @@ public static string CleanSpecialCharacters(this string input, string substitute
225221
/// <returns>New string</returns>
226222
public static string UppercaseFirst(this string input)
227223
{
228-
if(input == null)
229-
{
230-
return null;
231-
}
232-
233-
if(input == "")
224+
if(string.IsNullOrEmpty(input))
234225
{
235-
return "";
226+
return input;
236227
}
237228

238229
var charArray = input.ToCharArray();
@@ -247,19 +238,92 @@ public static string UppercaseFirst(this string input)
247238
/// <returns>Text transformed</returns>
248239
public static string LowercaseFirst(this string input)
249240
{
250-
if(input == null)
251-
{
252-
return null;
253-
}
254-
255-
if(input == "")
241+
if(string.IsNullOrEmpty(input))
256242
{
257-
return "";
243+
return input;
258244
}
259245

260246
var charArray = input.ToCharArray();
261247
charArray[0] = char.ToLower(charArray[0]);
262248
return new string(charArray);
263249
}
250+
251+
/// <summary>
252+
/// Convert a text to snake case format
253+
/// </summary>
254+
/// <param name="input">Text to be transformed</param>
255+
/// <returns>Text transformed</returns>
256+
public static string ToSnakeCase(this string input)
257+
{ // Reference1: https://stackoverflow.com/questions/63055621/how-to-convert-camel-case-to-snake-case-with-two-capitals-next-to-each-other
258+
// Reference1: https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs
259+
if(string.IsNullOrEmpty(input))
260+
{
261+
return input;
262+
}
263+
264+
var builder = new StringBuilder(input.Length + Math.Min(2, input.Length / 5));
265+
var previousCategory = default(UnicodeCategory?);
266+
267+
for(var currentIndex = 0; currentIndex < input.Length; currentIndex++)
268+
{
269+
var currentChar = input[currentIndex];
270+
if(currentChar == '_')
271+
{
272+
builder.Append('_');
273+
previousCategory = null;
274+
continue;
275+
}
276+
277+
var currentCategory = char.GetUnicodeCategory(currentChar);
278+
switch(currentCategory)
279+
{
280+
case UnicodeCategory.UppercaseLetter:
281+
case UnicodeCategory.TitlecaseLetter:
282+
_snakeCaseHandleUppercaseLetterAndTitlecaseLetter(
283+
input,
284+
currentIndex,
285+
previousCategory,
286+
builder
287+
);
288+
289+
currentChar = char.ToLower(currentChar, CultureInfo.InvariantCulture);
290+
break;
291+
292+
case UnicodeCategory.LowercaseLetter:
293+
case UnicodeCategory.DecimalDigitNumber:
294+
if(previousCategory == UnicodeCategory.SpaceSeparator)
295+
{
296+
builder.Append('_');
297+
}
298+
break;
299+
300+
default:
301+
if(previousCategory != null)
302+
{
303+
previousCategory = UnicodeCategory.SpaceSeparator;
304+
}
305+
continue;
306+
}
307+
308+
builder.Append(currentChar);
309+
previousCategory = currentCategory;
310+
}
311+
312+
return builder.ToString();
313+
}
314+
315+
private static void _snakeCaseHandleUppercaseLetterAndTitlecaseLetter(string input, int currentIndex, UnicodeCategory? previousCategory, StringBuilder builder)
316+
{
317+
if(previousCategory == UnicodeCategory.SpaceSeparator ||
318+
previousCategory == UnicodeCategory.LowercaseLetter ||
319+
(previousCategory != UnicodeCategory.DecimalDigitNumber &&
320+
previousCategory != null &&
321+
currentIndex > 0 &&
322+
currentIndex + 1 < input.Length &&
323+
char.IsLower(input[currentIndex + 1])))
324+
{
325+
builder.Append('_');
326+
}
327+
}
264328
}
265329
}

tests/PowerUtils.Text.Tests/PowerUtils.Text.Tests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020

2121

2222
<ItemGroup>
23-
<PackageReference Include="FluentAssertions" Version="6.4.0" />
24-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
23+
<PackageReference Include="FluentAssertions" Version="6.5.1" />
24+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
2525
<PackageReference Include="xunit" Version="2.4.1" />
2626
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
2727
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace PowerUtils.Text.Tests.TextExtensionsTests;
2+
3+
[Trait("Extension", "ToSnakeCase")]
4+
public class ToSnakeCaseTests
5+
{
6+
[Theory(DisplayName = "Converto a text to snake case")]
7+
[InlineData(null, null)]
8+
[InlineData("", "")]
9+
[InlineData("TestSC", "test_sc")]
10+
[InlineData("testSC", "test_sc")]
11+
[InlineData("TestSnakeCase", "test_snake_case")]
12+
[InlineData("testSnakeCase", "test_snake_case")]
13+
[InlineData("TestSnakeCase123", "test_snake_case123")]
14+
[InlineData("_testSnakeCase123", "_test_snake_case123")]
15+
[InlineData("test_SC", "test_sc")]
16+
[InlineData("Test", "test")]
17+
[InlineData("TEST", "test")]
18+
[InlineData("Test Snake Case", "test_snake_case")]
19+
[InlineData("TEST SNAKE CASE", "test_snake_case")]
20+
[InlineData("Test Snake Case", "test_snake_case")]
21+
[InlineData("Test SnakeCase", "test_snake_case")]
22+
[InlineData("Test Snake111 Case", "test_snake111_case")]
23+
[InlineData("TEST SNAKE111 CASE", "test_snake111_case")]
24+
[InlineData("Test Snake Case 111", "test_snake_case_111")]
25+
[InlineData("Test 123 Case 111", "test_123_case_111")]
26+
[InlineData("Test123 Case 111", "test123_case_111")]
27+
[InlineData("Test 123Case 111", "test_123case_111")]
28+
[InlineData("Test 123Case__", "test_123case__")]
29+
[InlineData("Test 123Case1__", "test_123case1__")]
30+
public void ToSnakeCase(string input, string expected)
31+
{
32+
// Arrange && Act
33+
var act = input.ToSnakeCase();
34+
35+
36+
// Assert
37+
act.Should()
38+
.Be(expected);
39+
}
40+
}

0 commit comments

Comments
 (0)