Skip to content

Commit c03f120

Browse files
ParseQueryString (#8)
* ParseQueryString
1 parent ce61e73 commit c03f120

6 files changed

Lines changed: 185 additions & 6 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@ Search for especial characters (not A to Z and not 0 to 9) in the string
140140
var result = "12.8/0';@#!%^&*()a12,9abc".OnlySpecialCharacters(); //"./';@#!%^&*(),"
141141
```
142142

143+
### `ParseQueryString`
144+
145+
Parses a query string into a dictionary of key-value pairs.
146+
147+
**Usage**
148+
```csharp
149+
var queryString = "?name=JohnDoe&age=30&isMember=true";
150+
var result = queryString.ParseQueryString(autoConvertType: true);
151+
// result will be a dictionary with keys "name", "age", "isMember"
152+
// and corresponding values "JohnDoe", 30, true
153+
```
154+
143155
## Contributing 👥
144156

145157
Contributions are welcome! If you find a bug or have a feature request, please open an issue on GitHub.

docs/NuGet.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,16 @@ Search for especial characters (not A to Z and not 0 to 9) in the string
132132
**Usage**
133133
```csharp
134134
var result = "12.8/0';@#!%^&*()a12,9abc".OnlySpecialCharacters(); //"./';@#!%^&*(),"
135+
```
136+
137+
### `ParseQueryString`
138+
139+
Parses a query string into a dictionary of key-value pairs.
140+
141+
**Usage**
142+
```csharp
143+
var queryString = "?name=JohnDoe&age=30&isMember=true";
144+
var result = queryString.ParseQueryString(autoConvertType: true);
145+
// result will be a dictionary with keys "name", "age", "isMember"
146+
// and corresponding values "JohnDoe", 30, true
135147
```

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "6.0.0",
3+
"version": "8.0.0",
44
"rollForward": "latestMinor",
55
"allowPrerelease": false
66
}

src/FunctionalStringExtensions.cs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
24
using System.Linq;
35
using System.Text;
46
using System.Text.RegularExpressions;
57
using System.Threading.Tasks;
8+
using System.Web;
69

710
namespace FunctionalStringExtensions;
811

912
public static class FunctionalStringExtensions
1013
{
1114
private static readonly object Lock = new();
1215
private static readonly TimeSpan RegexTimeOut = TimeSpan.FromMilliseconds(100);
16+
private static readonly IDictionary<string, object> EmptyQueryDictionary = new Dictionary<string, object>(0);
1317

1418
/// <summary>
1519
/// This extension method returns the provided default value if the input string is null or empty.
@@ -110,7 +114,7 @@ public static T ToEnum<T>(this string? value, T @default = default) where T : st
110114
/// <summary>
111115
/// Search for letters (A to Z) in the string
112116
/// </summary>
113-
/// <param name="value">input string</param>
117+
/// <param name="value">Input <see cref="string"/> value</param>
114118
/// <returns>The letters present in the string provided</returns>
115119
public static string OnlyLetters(this string? value)
116120
{
@@ -120,7 +124,7 @@ public static string OnlyLetters(this string? value)
120124
/// <summary>
121125
/// Search for numbers (0 to 9) in the string
122126
/// </summary>
123-
/// <param name="value">input string</param>
127+
/// <param name="value">Input <see cref="string"/> value</param>
124128
/// <returns>The numbers present in the string provided</returns>
125129
public static string OnlyNumbers(this string? value)
126130
{
@@ -130,7 +134,7 @@ public static string OnlyNumbers(this string? value)
130134
/// <summary>
131135
/// Search for characters and numbers (A to Z or 0 to 9) in the string
132136
/// </summary>
133-
/// <param name="value">input string</param>
137+
/// <param name="value">Input <see cref="string"/> value</param>
134138
/// <returns>The characters and numbers present in the string provided</returns>
135139
public static string OnlyCharactersAndNumbers(this string? value)
136140
{
@@ -140,12 +144,77 @@ public static string OnlyCharactersAndNumbers(this string? value)
140144
/// <summary>
141145
/// Search for especial characters (not A to Z and not 0 to 9) in the string
142146
/// </summary>
143-
/// <param name="value">input string</param>
147+
/// <param name="value">Input <see cref="string"/> value</param>
144148
/// <returns>The especial characters present in the string provided</returns>
145149
public static string OnlySpecialCharacters(this string? value)
146150
{
147151
return value.FilterCharacters(c => !char.IsLetterOrDigit(c));
148152
}
153+
154+
/// <summary>
155+
/// Parses a query string into a dictionary of key-value pairs.
156+
/// </summary>
157+
/// <param name="value">The input query string.</param>
158+
/// <param name="autoConvertType">Indicates whether to automatically convert the query values to their appropriate types (e.g., int, bool, double). Default is false.</param>
159+
/// <returns>
160+
/// A dictionary containing the parsed query parameters as key-value pairs. The keys are strings, and the values are objects.
161+
/// If the input string is null or empty, an empty dictionary is returned.
162+
/// </returns>
163+
public static IDictionary<string, object> ParseQueryString(this string? value, bool autoConvertType = false)
164+
{
165+
var index = value?.IndexOf('?') ?? -1;
166+
if (string.IsNullOrEmpty(value) || index < 0)
167+
{
168+
return EmptyQueryDictionary;
169+
}
170+
171+
var query = value[0] == '?'
172+
? value.TrimStart('?')
173+
: value.Substring(index + 1, value.Length - index - 1);
174+
175+
if (string.IsNullOrEmpty(query))
176+
{
177+
return EmptyQueryDictionary;
178+
}
179+
180+
return query
181+
.Split('&', StringSplitOptions.RemoveEmptyEntries)
182+
.Select(part => part.Split('='))
183+
.ToDictionary(
184+
part => HttpUtility.UrlDecode(part[0]),
185+
part =>
186+
{
187+
var partValue = MaybeGetValue(part);
188+
var decodedValue = HttpUtility.UrlDecode(partValue);
189+
return autoConvertType ? ConvertToType(decodedValue) : decodedValue;
190+
});
191+
192+
string MaybeGetValue(string[] partValue)
193+
{
194+
return partValue.Length < 2 ? string.Empty : partValue[1];
195+
}
196+
197+
object ConvertToType(string valueToConvert)
198+
{
199+
if (int.TryParse(valueToConvert, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
200+
{
201+
return intValue;
202+
}
203+
204+
if (bool.TryParse(valueToConvert, out var boolValue))
205+
{
206+
return boolValue;
207+
}
208+
209+
if (double.TryParse(valueToConvert, NumberStyles.Float | NumberStyles.AllowThousands,
210+
CultureInfo.InvariantCulture, out var doubleValue))
211+
{
212+
return doubleValue;
213+
}
214+
215+
return valueToConvert;
216+
}
217+
}
149218

150219
private static string FilterCharacters(this string? value, Func<char, bool> predicate)
151220
{

tests/FunctionalStringExtensionsTests/FunctionalStringExtensionsTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,92 @@ public void OnlySpecialCharactersShouldReturnOnlySpecialCharacters(string? value
272272
//Assert
273273
result.Should().Be(expected);
274274
}
275+
276+
[Theory]
277+
[InlineData(null)]
278+
[InlineData("")]
279+
[InlineData("not a valid url")]
280+
[InlineData("?")]
281+
public void ParseQueryString_ShouldReturnEmptyListWhenNotAValidInput(string? value)
282+
{
283+
var result = value.ParseQueryString();
284+
285+
result.Should().BeEmpty();
286+
}
287+
288+
[Theory]
289+
[InlineData("http://yoursite.com?variable1=false", "variable1", false)]
290+
[InlineData("http://yoursite.com?variable1=False", "variable1", false)]
291+
[InlineData("http://yoursite.com?variable1=1", "variable1", 1)]
292+
[InlineData("http://yoursite.com?variable1=19.86", "variable1", 19.86)]
293+
[InlineData("?variable1=true", "variable1", true)]
294+
[InlineData("?variable1=True", "variable1", true)]
295+
[InlineData("?variable1=1", "variable1", 1)]
296+
[InlineData("?variable1=0.77", "variable1", 0.77)]
297+
[InlineData("?variable1=test", "variable1", "test")]
298+
public void ParseQueryString_ConvertType_ShouldReturnListOfKeyValues(string? value, string expectedKey, object expectedValue)
299+
{
300+
var result = value.ParseQueryString(autoConvertType: true);
301+
302+
result.Should().NotBeEmpty();
303+
result.Should().HaveCount(1);
304+
result.First().Key.Should().Be(expectedKey);
305+
result.First().Value.Should().Be(expectedValue);
306+
result.First().Value.Should().BeOfType(expectedValue.GetType());
307+
}
308+
309+
[Theory]
310+
[InlineData("http://yoursite.com?variable1=false", "variable1", "false")]
311+
[InlineData("http://yoursite.com?variable1=False", "variable1", "False")]
312+
[InlineData("http://yoursite.com?variable1=1", "variable1", "1")]
313+
[InlineData("http://yoursite.com?variable1=19.86", "variable1", "19.86")]
314+
[InlineData("?variable1=true", "variable1", "true")]
315+
[InlineData("?variable1=True", "variable1", "True")]
316+
[InlineData("?variable1=1", "variable1", "1")]
317+
[InlineData("?variable1=0.77", "variable1", "0.77")]
318+
[InlineData("?variable1=test", "variable1", "test")]
319+
public void ParseQueryString_WithoutConversion_ShouldReturnListOfKeyValues(string? value, string expectedKey, string expectedValue)
320+
{
321+
var result = value.ParseQueryString(autoConvertType: false);
322+
323+
result.Should().NotBeEmpty();
324+
result.Should().HaveCount(1);
325+
result.First().Key.Should().Be(expectedKey);
326+
result.First().Value.Should().Be(expectedValue);
327+
result.First().Value.Should().BeOfType<string>();
328+
}
329+
330+
[Theory]
331+
[InlineData("?variable1=15&variable2=is that a question?&variable3=true&variable4=33.88&variable5=", "variable1=15|variable2=is that a question?|variable3=true|variable4=33.88|variable5=")]
332+
public void ParseQueryString_ShouldReturnListOfKeyValues(string? value, string expectedPipedString)
333+
{
334+
var result = value.ParseQueryString(autoConvertType: false);
335+
336+
var expected = expectedPipedString
337+
.Split('|', StringSplitOptions.RemoveEmptyEntries)
338+
.Select(s => s.Split('='))
339+
.ToDictionary(k => k[0], v => v[1]);
340+
341+
result.Should().NotBeEmpty();
342+
result.Should().BeEquivalentTo(expected);
343+
}
344+
345+
[Theory]
346+
[InlineData("?not a valid url", "not a valid url=")]
347+
[InlineData("?not a valid url&&&&&", "not a valid url=")]
348+
[InlineData("?not a valid url&&&&&=value", "not a valid url=|=value")]
349+
public void ParseQueryString_EdgeCasesShouldReturnListOfKeyValues(string? value, string expectedPipedString)
350+
{
351+
var result = value.ParseQueryString(autoConvertType: false);
352+
353+
var expected = expectedPipedString
354+
.Split('|')
355+
.Select(s => s.Split('='))
356+
.ToDictionary(k => k.Length > 0? k[0] : string.Empty, v => v.Length > 1 ? v[1]: string.Empty);
357+
358+
result.Should().NotBeEmpty();
359+
result.Should().BeEquivalentTo(expected);
360+
}
275361
}
276362

277363
public enum FakeEnum

tests/FunctionalStringExtensionsTests/FunctionalStringExtensionsTests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net8.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

0 commit comments

Comments
 (0)