diff --git a/apiCount.include.md b/apiCount.include.md index 5779f1e8..d585df9e 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1,4 +1,4 @@ -**API count: 897** +**API count: 892** ### Per Target Framework diff --git a/api_list.include.md b/api_list.include.md index 68563e87..de016dae 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -688,6 +688,7 @@ * `string GetRelativePath(string, string)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.getrelativepath?view=net-11.0) * `bool HasExtension(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.getfilenamewithoutextension?view=net-11.0#system-io-path-getfilenamewithoutextension(system-readonlyspan((system-char)))) * `bool IsPathRooted(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.ispathrooted?view=net-11.0#system-io-path-ispathrooted(system-readonlyspan((system-char)))) + * `string Join(string?[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.join?view=net-11.0#system-io-path-join(system-string())) * `ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.trimendingdirectoryseparator?view=net-11.0#system-io-path-trimendingdirectoryseparator(system-readonlyspan((system-char)))) * `string TrimEndingDirectorySeparator(string)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.trimendingdirectoryseparator?view=net-11.0#system-io-path-trimendingdirectoryseparator(system-string)) diff --git a/readme.md b/readme.md index 42d301ba..7a92c2a1 100644 --- a/readme.md +++ b/readme.md @@ -1219,6 +1219,7 @@ The class `Polyfill` includes the following extension methods: * `string GetRelativePath(string, string)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.getrelativepath?view=net-11.0) * `bool HasExtension(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.getfilenamewithoutextension?view=net-11.0#system-io-path-getfilenamewithoutextension(system-readonlyspan((system-char)))) * `bool IsPathRooted(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.ispathrooted?view=net-11.0#system-io-path-ispathrooted(system-readonlyspan((system-char)))) + * `string Join(string?[])` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.join?view=net-11.0#system-io-path-join(system-string())) * `ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.trimendingdirectoryseparator?view=net-11.0#system-io-path-trimendingdirectoryseparator(system-readonlyspan((system-char)))) * `string TrimEndingDirectorySeparator(string)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.trimendingdirectoryseparator?view=net-11.0#system-io-path-trimendingdirectoryseparator(system-string)) diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index 0e3ef5ef..bd993f14 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -781,6 +781,7 @@ void Stopwatch_Methods() void Path_Methods() { var relative = Path.GetRelativePath("/folder1/folder2", "/folder1/folder3"); + var joined = Path.Join("a", "b", "c"); #if FeatureMemory var rooted = Path.IsPathRooted("/root".AsSpan()); #endif diff --git a/src/Polyfill/PathPolyfill.cs b/src/Polyfill/PathPolyfill.cs index ed32de49..9afe2a3b 100644 --- a/src/Polyfill/PathPolyfill.cs +++ b/src/Polyfill/PathPolyfill.cs @@ -110,6 +110,39 @@ public static string TrimEndingDirectorySeparator(string path) return path; } + + /// + /// Concatenates a span of paths into a single path. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.path.join?view=net-11.0#system-io-path-join(system-string()) + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + + var builder = new System.Text.StringBuilder(); + + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + + builder.Append(path); + } + + return builder.ToString(); + } #endif static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; diff --git a/src/Split/net461/PathPolyfill.cs b/src/Split/net461/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/net461/PathPolyfill.cs +++ b/src/Split/net461/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/net462/PathPolyfill.cs b/src/Split/net462/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/net462/PathPolyfill.cs +++ b/src/Split/net462/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/net47/PathPolyfill.cs b/src/Split/net47/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/net47/PathPolyfill.cs +++ b/src/Split/net47/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/net471/PathPolyfill.cs b/src/Split/net471/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/net471/PathPolyfill.cs +++ b/src/Split/net471/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/net472/PathPolyfill.cs b/src/Split/net472/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/net472/PathPolyfill.cs +++ b/src/Split/net472/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/net48/PathPolyfill.cs b/src/Split/net48/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/net48/PathPolyfill.cs +++ b/src/Split/net48/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/net481/PathPolyfill.cs b/src/Split/net481/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/net481/PathPolyfill.cs +++ b/src/Split/net481/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/netcoreapp2.0/PathPolyfill.cs b/src/Split/netcoreapp2.0/PathPolyfill.cs index 760f40d4..e8b4ba38 100644 --- a/src/Split/netcoreapp2.0/PathPolyfill.cs +++ b/src/Split/netcoreapp2.0/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/netcoreapp2.1/PathPolyfill.cs b/src/Split/netcoreapp2.1/PathPolyfill.cs index dc917864..1c3df6f9 100644 --- a/src/Split/netcoreapp2.1/PathPolyfill.cs +++ b/src/Split/netcoreapp2.1/PathPolyfill.cs @@ -47,6 +47,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/netcoreapp2.2/PathPolyfill.cs b/src/Split/netcoreapp2.2/PathPolyfill.cs index dc917864..1c3df6f9 100644 --- a/src/Split/netcoreapp2.2/PathPolyfill.cs +++ b/src/Split/netcoreapp2.2/PathPolyfill.cs @@ -47,6 +47,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/netstandard2.0/PathPolyfill.cs b/src/Split/netstandard2.0/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/netstandard2.0/PathPolyfill.cs +++ b/src/Split/netstandard2.0/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/netstandard2.1/PathPolyfill.cs b/src/Split/netstandard2.1/PathPolyfill.cs index dc917864..1c3df6f9 100644 --- a/src/Split/netstandard2.1/PathPolyfill.cs +++ b/src/Split/netstandard2.1/PathPolyfill.cs @@ -47,6 +47,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Split/uap10.0/PathPolyfill.cs b/src/Split/uap10.0/PathPolyfill.cs index 1bf2d728..137bb99a 100644 --- a/src/Split/uap10.0/PathPolyfill.cs +++ b/src/Split/uap10.0/PathPolyfill.cs @@ -77,6 +77,32 @@ public static string TrimEndingDirectorySeparator(string path) } return path; } + /// + /// Concatenates a span of paths into a single path. + /// + public static string Join(params string?[] paths) + { + if (paths.Length == 0) + { + return string.Empty; + } + var builder = new System.Text.StringBuilder(); + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + if (builder.Length > 0 && + !IsDirectorySeparator(builder[builder.Length - 1]) && + !IsDirectorySeparator(path![0])) + { + builder.Append(Path.DirectorySeparatorChar); + } + builder.Append(path); + } + return builder.ToString(); + } static bool IsRoot(string path) => Path.IsPathRooted(path) && Path.GetDirectoryName(path) == null; static bool IsDirectorySeparator(char c) => diff --git a/src/Tests/PathTests.cs b/src/Tests/PathTests.cs index e919e054..ab31acfa 100644 --- a/src/Tests/PathTests.cs +++ b/src/Tests/PathTests.cs @@ -112,6 +112,23 @@ public async Task GetRelativePath_ThrowsOnEmpty() await Assert.That(() => Path.GetRelativePath("path", "")).Throws(); } + [Test] + public async Task Join() + { + await Assert.That(Path.Join("folder1", "folder2", "file.txt")) + .IsEqualTo($"folder1{Path.DirectorySeparatorChar}folder2{Path.DirectorySeparatorChar}file.txt"); + + await Assert.That(Path.Join("folder/", "file.txt")).IsEqualTo("folder/file.txt"); + + await Assert.That(Path.Join("folder", "", "file.txt")) + .IsEqualTo($"folder{Path.DirectorySeparatorChar}file.txt"); + + await Assert.That(Path.Join("folder", null, "file.txt")) + .IsEqualTo($"folder{Path.DirectorySeparatorChar}file.txt"); + + await Assert.That(Path.Join(Array.Empty())).IsEqualTo(string.Empty); + } + [Test] public async Task EndsInDirectorySeparator() {