From 47c7fb0238e13e912f24e6e5c973276964ef5647 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sun, 20 Sep 2026 23:06:01 +0200 Subject: [PATCH] (#153) PathExtensions: document WithExtension, add tests --- TruePath.Tests/PathExtensionsTests.cs | 22 ++++++++ TruePath/PathExtensions.cs | 72 +++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/TruePath.Tests/PathExtensionsTests.cs b/TruePath.Tests/PathExtensionsTests.cs index 6fec2a4..b807d88 100644 --- a/TruePath.Tests/PathExtensionsTests.cs +++ b/TruePath.Tests/PathExtensionsTests.cs @@ -100,6 +100,28 @@ public void WithExtensionTests_Windows(string inputPath, string newExtension, st Assert.Equal(expected, newPath.Value); } + [Theory] + [InlineData("file.txt", "md", "file.md")] + [InlineData("file.txt", ".md", "file.md")] + [InlineData("file", "md", "file.md")] + [InlineData("file", ".md", "file.md")] + [InlineData("archive.tar.gz", "zip", "archive.tar.zip")] + [InlineData("archive.zip", ".tar.gz", "archive.tar.gz")] + [InlineData("file.txt", "", "file.")] + [InlineData("file.txt", null, "file")] + [InlineData(".gitignore", "hgignore", ".hgignore")] + public void WithExtensionArgumentTests(string inputPath, string? newExtension, string expected) + { + // Arrange + var path = new LocalPath(inputPath); + + // Act + var newPath = path.WithExtension(newExtension); + + // Assert + Assert.Equal(expected, newPath.Value); + } + [Theory] [InlineData("/", "bar", "/.bar")] [InlineData("/file", "bar", "/file.bar")] diff --git a/TruePath/PathExtensions.cs b/TruePath/PathExtensions.cs index 2ba4187..1131de2 100644 --- a/TruePath/PathExtensions.cs +++ b/TruePath/PathExtensions.cs @@ -57,11 +57,33 @@ public static string GetFilenameWithoutExtension(this IPath path) => /// /// The type of the path, which must implement . /// The original path. - /// The new extension to apply. + /// + /// + /// The new extension, either with or without the leading dot: both txt and .txt give the + /// same result, since the dot is added when missing. + /// + /// + /// Pass to remove the extension entirely (file.txt becomes file), or + /// an empty string to remove it but keep the trailing dot (file.txt becomes file.). + /// + /// /// /// A new path of type with the modified file name component. /// The original object is not modified. /// + /// + /// + /// Only the last extension is replaced: for archive.tar.gz and the extension zip, the result + /// is archive.tar.zip. The extension itself may contain dots, so .tar.gz applied to + /// archive.zip gives archive.tar.gz. + /// + /// If the file name has no extension, the new one is appended: file becomes file.txt. + /// + /// A file name consisting entirely of an extension is treated as an extension, consistently with + /// : .gitignore with the extension hgignore becomes + /// .hgignore. + /// + /// public static TPath WithExtension(this TPath path, string? extension) where TPath : IPath => TPath.Create(Path.ChangeExtension(((IPath)path).Value, extension)); #else @@ -70,11 +92,33 @@ public static TPath WithExtension(this TPath path, string? extension) whe /// or with a new extension-like component if the original file name was empty. /// /// The original path. - /// The new extension to apply. + /// + /// + /// The new extension, either with or without the leading dot: both txt and .txt give the + /// same result, since the dot is added when missing. + /// + /// + /// Pass to remove the extension entirely (file.txt becomes file), or + /// an empty string to remove it but keep the trailing dot (file.txt becomes file.). + /// + /// /// /// A new path of type with the modified file name component. /// The original object is not modified. /// + /// + /// + /// Only the last extension is replaced: for archive.tar.gz and the extension zip, the result + /// is archive.tar.zip. The extension itself may contain dots, so .tar.gz applied to + /// archive.zip gives archive.tar.gz. + /// + /// If the file name has no extension, the new one is appended: file becomes file.txt. + /// + /// A file name consisting entirely of an extension is treated as an extension, consistently with + /// : .gitignore with the extension hgignore becomes + /// .hgignore. + /// + /// public static AbsolutePath WithExtension(this AbsolutePath path, string? extension) => AbsolutePath.Create(Path.ChangeExtension(path.Value, extension)); @@ -83,11 +127,33 @@ public static AbsolutePath WithExtension(this AbsolutePath path, string? extensi /// or with a new extension-like component if the original file name was empty. /// /// The original path. - /// The new extension to apply. + /// + /// + /// The new extension, either with or without the leading dot: both txt and .txt give the + /// same result, since the dot is added when missing. + /// + /// + /// Pass to remove the extension entirely (file.txt becomes file), or + /// an empty string to remove it but keep the trailing dot (file.txt becomes file.). + /// + /// /// /// A new path of type with the modified file name component. /// The original object is not modified. /// + /// + /// + /// Only the last extension is replaced: for archive.tar.gz and the extension zip, the result + /// is archive.tar.zip. The extension itself may contain dots, so .tar.gz applied to + /// archive.zip gives archive.tar.gz. + /// + /// If the file name has no extension, the new one is appended: file becomes file.txt. + /// + /// A file name consisting entirely of an extension is treated as an extension, consistently with + /// : .gitignore with the extension hgignore becomes + /// .hgignore. + /// + /// public static LocalPath WithExtension(this LocalPath path, string? extension) => LocalPath.Create(Path.ChangeExtension(path.Value, extension)); #endif