From 2d5df29d80a80526302777860b6810d1fde50b78 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:53:46 +0900 Subject: [PATCH 01/49] Update to .NET 10 --- .../UniqueFileGenerator.Console.fsproj | 2 +- src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj index f1b5ea6..bb57cf1 100644 --- a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj +++ b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 3579 true diff --git a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj index 826a6aa..4c6f5c7 100644 --- a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj +++ b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 false false From 52478691662dbb2e5e715df702f460fd4d11eda8 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:56:30 +0900 Subject: [PATCH 02/49] Update packages --- .../UniqueFileGenerator.Console.fsproj | 3 ++- .../UniqueFileGenerator.Tests.fsproj | 13 ++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj index bb57cf1..aaeb492 100644 --- a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj +++ b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj @@ -21,8 +21,9 @@ - + + diff --git a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj index 4c6f5c7..f2f88c8 100644 --- a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj +++ b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj @@ -13,10 +13,17 @@ - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + From e198481a6db87a1d42aa271bc41d3d44ef5dded3 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:57:43 +0900 Subject: [PATCH 03/49] Add CCFSharpUtils --- .../UniqueFileGenerator.Console.fsproj | 1 + src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj | 1 + 2 files changed, 2 insertions(+) diff --git a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj index aaeb492..402401e 100644 --- a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj +++ b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj @@ -20,6 +20,7 @@ + diff --git a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj index f2f88c8..4166da2 100644 --- a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj +++ b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj @@ -13,6 +13,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive From ee8ac5d689564a071d138e61dbd643eef41ea987 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:54:22 +0900 Subject: [PATCH 04/49] Add separator tests --- .../ArgParserTests.fs | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index efd8da7..10a324f 100644 --- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs +++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs @@ -244,3 +244,138 @@ let ``Success when valid file count with prefix, extension, base length, custom Delay = int validOptionValues[Delay] } let actual = validate args Assert.Equal(expected, actual) + +module SupportedSeparators = + + [] + let ``Strips commas from text`` () = + let text = "hello,world" + let result = stripSeparators text + + Assert.Equal("helloworld", result) + + [] + let ``Strips underscores from text`` () = + let text = "hello_world" + let result = stripSeparators text + + Assert.Equal("helloworld", result) + + [] + let ``Strips both commas and underscores`` () = + let text = "hello,world_test" + let result = stripSeparators text + + Assert.Equal("helloworldtest", result) + + [] + let ``Strips multiple consecutive separators`` () = + let text = "hello,,__world" + let result = stripSeparators text + + Assert.Equal("helloworld", result) + + [] + let ``Returns empty string when input is only separators`` () = + let text = ",_,_," + let result = stripSeparators text + + Assert.Equal(String.Empty, result) + + [] + let ``Returns empty string for empty input`` () = + let text = String.Empty + let result = stripSeparators text + + Assert.Equal(String.Empty, result) + + [] + let ``Returns unchanged text with no separators`` () = + let text = "helloworld" + let result = stripSeparators text + + Assert.Equal("helloworld", result) + + [] + let ``Preserves whitespace`` () = + let text = "hello , world _ test" + let result = stripSeparators text + + Assert.Equal("hello world test", result) + + [] + let ``Preserves other punctuation`` () = + let text = "hello,world.test_example!done" + let result = stripSeparators text + + Assert.Equal("helloworld.testexample!done", result) + + [] + let ``Preserves numbers`` () = + let text = "test_123,456_abc" + let result = stripSeparators text + + Assert.Equal("test123456abc", result) + + [] + let ``Handles mixed case correctly`` () = + let text = "Hello,World_Test" + let result = stripSeparators text + + Assert.Equal("HelloWorldTest", result) + + [] + let ``Handles Unicode characters`` () = + let text = "café,naïve_résumé,東京と京都" + let result = stripSeparators text + + Assert.Equal("cafénaïverésumé東京と京都", result) + + [] + let ``Handles single character input`` () = + let text = "_" + let result = stripSeparators text + + Assert.Equal(String.Empty, result) + + [] + let ``Handles single character without separator`` () = + let text = "a" + let result = stripSeparators text + + Assert.Equal("a", result) + + [] + let ``Handles separators at start`` () = + let text = "_,hello" + let result = stripSeparators text + + Assert.Equal("hello", result) + + [] + let ``Handles separators at end`` () = + let text = "hello_," + let result = stripSeparators text + + Assert.Equal("hello", result) + + [] + let ``Handles separators at both ends`` () = + let text = ",_hello_," + let result = stripSeparators text + + Assert.Equal("hello", result) + + [] + let ``Handles large strings`` () = + let text = String.replicate 1000 "a_b," + let result = stripSeparators text + + Assert.Equal(String.replicate 1000 "ab", result) + + [] + let ``Handles tabs and newlines`` () = + let text = "hello,world\ttest_example\nmore" + let result = stripSeparators text + + Assert.Equal("helloworld\ttestexample\nmore", result) From 8a6ebab5dad0099ac5d63ad88a1d810049ab9d68 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:55:01 +0900 Subject: [PATCH 05/49] Use String.stripSubstrings from my library --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index eba7210..5a17254 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -4,13 +4,12 @@ open Errors open Utilities open System open FsToolkit.ErrorHandling +open CCFSharpUtils.Text module ArgTypes = - let stripSeparators text : string = - let supportedSeparators = [ ","; "_" ] + let supportedSeparators = [ ","; "_" ] - (text, supportedSeparators) - ||> List.fold (fun acc s -> acc.Replace(s, String.Empty)) + let stripSeparators = String.stripSubstrings supportedSeparators let private tryParseIntInRange (floor, ceiling) text = text From 2f843af03deba6754525b02aa380937180a94edf Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:39:38 +0900 Subject: [PATCH 06/49] Combine Option.map calls --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 5a17254..ed0ac12 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -19,7 +19,7 @@ module ArgTypes = type FileCount = private FileCount of int with static member val AllowedRange = 1, Int32.MaxValue - static member Create (text: string) = + static member Create(text: string) : Result = text |> stripSeparators |> parseInRange FileCount.AllowedRange @@ -45,8 +45,9 @@ module ArgTypes = static member TryCreate (text: string option) = text - |> Option.map stripSeparators - |> Option.map (fun arg -> arg.Trim() |> tryParseIntInRange NameBaseLength.AllowedRange) + |> Option.map (stripSeparators + >> String.trim + >> tryParseIntInRange NameBaseLength.AllowedRange) |> Option.defaultValue (Ok NameBaseLength.Default) |> Result.map NameBaseLength From 039e5ba57d4b8f17be4e24141e619f76db664e1f Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:22:38 +0900 Subject: [PATCH 07/49] Use the F#+ option keyword --- .../Arguments/ArgumentTypes.fs | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index ed0ac12..a6b89a0 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -3,7 +3,7 @@ namespace UniqueFileGenerator.Console open Errors open Utilities open System -open FsToolkit.ErrorHandling +open FSharpPlus open CCFSharpUtils.Text module ArgTypes = @@ -19,7 +19,7 @@ module ArgTypes = type FileCount = private FileCount of int with static member val AllowedRange = 1, Int32.MaxValue - static member Create(text: string) : Result = + static member Create text : Result = text |> stripSeparators |> parseInRange FileCount.AllowedRange @@ -31,10 +31,9 @@ module ArgTypes = type Prefix = private Prefix of string with static member val Default = String.Empty - static member Create (text: string option) = - match text with - | None -> Prefix.Default - | Some x -> x + static member Create text = + text + |> option id Prefix.Default |> Prefix member this.Value = let (Prefix prefix) = this in prefix @@ -43,12 +42,13 @@ module ArgTypes = static member val AllowedRange = 1, 100 static member val Default = 50 - static member TryCreate (text: string option) = + static member TryCreate text = text - |> Option.map (stripSeparators - >> String.trim - >> tryParseIntInRange NameBaseLength.AllowedRange) - |> Option.defaultValue (Ok NameBaseLength.Default) + |> option + (stripSeparators + >> String.trim + >> tryParseIntInRange NameBaseLength.AllowedRange) + (Ok NameBaseLength.Default) |> Result.map NameBaseLength member this.Value = let (NameBaseLength length) = this in length @@ -56,10 +56,9 @@ module ArgTypes = type Extension = private Extension of string with static member val Default = String.Empty - static member Create (text: string option) = - match text with - | None -> Extension.Default - | Some x -> x.Trim() + static member Create text = + text + |> option String.trim Extension.Default |> Extension member this.Value = let (Extension extension) = this in extension @@ -67,10 +66,9 @@ module ArgTypes = type OutputDirectory = private OutputDirectory of string with static member val Default = "output" - static member Create (text: string option) = - match text with - | None -> OutputDirectory.Default - | Some x -> x.Trim() + static member Create text = + text + |> option String.trim OutputDirectory.Default |> OutputDirectory member this.Value = let (OutputDirectory dir) = this in dir @@ -78,14 +76,15 @@ module ArgTypes = type Size = private Size of int option with static member val AllowedRange = 1, Int32.MaxValue - static member TryCreate (text: string option) = + static member TryCreate text = text - |> Option.map stripSeparators - |> Option.map (fun arg -> arg.Trim() |> tryParseIntInRange Size.AllowedRange) + |> Option.map (stripSeparators + >> String.trim + >> tryParseIntInRange Size.AllowedRange) |> function - | Some (Ok i) -> Ok (Size (Some i)) + | Some (Ok i) -> Ok (Size (Some i)) | Some (Error e) -> Error e // Parse error. - | None -> Ok (Size None) // No size entered. + | None -> Ok (Size None) // No size entered. member this.Value = let (Size size) = this in size @@ -93,11 +92,13 @@ module ArgTypes = static member val AllowedRange = 0, Int32.MaxValue static member val Default = 0 - static member TryCreate (text: string option) = + static member TryCreate text = text - |> Option.map stripSeparators - |> Option.map (fun arg -> arg.Trim() |> tryParseIntInRange Delay.AllowedRange) - |> Option.defaultValue (Ok Delay.Default) + |> option + (stripSeparators + >> String.trim + >> tryParseIntInRange Delay.AllowedRange) + (Ok Delay.Default) |> Result.map Delay member this.Value = let (Delay length) = this in length From 00bcd7ad8fedf1dd59d29ebb8cce930d2059fa0e Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:23:14 +0900 Subject: [PATCH 08/49] Use String.hasNoText --- src/UniqueFileGenerator.Console/StringGeneration.fs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index 433cc3b..821a113 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -1,6 +1,8 @@ namespace UniqueFileGenerator.Console open System +open FSharpPlus +open CCFSharpUtils.Text module StringGeneration = type FileNameParts = @@ -21,10 +23,11 @@ module StringGeneration = let generateMultiple eachLength count : string array = Array.init count (fun _ -> generateSingle eachLength) + // TODO: Consider returning an actual file object. let toFileName parts : string = let sanitizedExtension = match parts.Ext.Trim() with - | ext when String.IsNullOrWhiteSpace ext -> String.Empty + | ext when String.hasNoText ext -> String.Empty | ext when ext.StartsWith '.' -> ext | ext -> $".%s{ext}" From 7e70cbed22876c08387311f301d7e0fec255046a Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:23:23 +0900 Subject: [PATCH 09/49] Use option keyword --- src/UniqueFileGenerator.Console/StringGeneration.fs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index 821a113..aaffd36 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -37,6 +37,4 @@ module StringGeneration = sanitizedExtension) let generateFileContent sizeInBytes fallback : string = - sizeInBytes - |> Option.map generateSingle - |> Option.defaultValue fallback + sizeInBytes |> option generateSingle fallback From 8d24c2c716ca111b9323a208e2374671180438b9 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:10:32 +0900 Subject: [PATCH 10/49] Update package --- .../UniqueFileGenerator.Console.fsproj | 2 +- src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj index 402401e..939946a 100644 --- a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj +++ b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj @@ -20,7 +20,7 @@ - + diff --git a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj index 4166da2..886a5a3 100644 --- a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj +++ b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 58d41096b830f33502477ee965f6c26472d43ba0 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:12:28 +0900 Subject: [PATCH 11/49] Use Num.isEven --- .../Arguments/ArgumentValidation.fs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index 62daa4c..674a848 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -3,16 +3,15 @@ namespace UniqueFileGenerator.Console open UniqueFileGenerator.Console open System open Errors +open CCFSharpUtils open FsToolkit.ErrorHandling open ArgTypes module ArgValidation = let private verifyArgCount (args: string array) = - let isEven i = i % 2 = 0 - match args.Length with | 0 -> Error NoArgsPassed - | l when isEven l -> Error ArgCountInvalid + | l when Num.isEven l -> Error ArgCountInvalid | _ -> Ok () let private toPairs (argPairs: string array) = From 4d480efcec355b2a259693c8a68dd7b9eda3dc13 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:23:49 +0900 Subject: [PATCH 12/49] Various updates --- .../Arguments/ArgumentValidation.fs | 34 +++++++------------ src/UniqueFileGenerator.Console/Errors.fs | 4 +-- src/UniqueFileGenerator.Console/Io.fs | 29 ++++++++-------- .../ArgParserTests.fs | 2 +- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index 674a848..e57109f 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -24,34 +24,26 @@ module ArgValidation = |> Array.chunkBySize 2 // Will throw if array length is odd! |> Array.map (fun x -> x[0].ToLowerInvariant(), x[1]) |> fun pairs -> - match pairs |> Array.map fst |> hasDuplicate with - | true -> Error DuplicateFlags - | false -> Ok (Map.ofArray pairs) + if pairs |> Array.map fst |> hasDuplicate + then Error DuplicateFlags + else Ok (Map.ofArray pairs) let private verifyOptionArgs (optionPairs: Map) = let hasMalformedOption optionPairs = let isCorrectFormat (o: string) = - o.Length = 2 && - o.StartsWith "-" && - Char.IsLetter o[1] + o.Length = 2 && o.StartsWith "-" && Char.IsLetter o[1] optionPairs |> Seq.forall isCorrectFormat |> not - let hasUnsupportedOption options = - let isUnsupported option = - flags - |> Map.values - |> Seq.contains option - |> not - - options - |> Seq.exists isUnsupported + let hasUnknownOption options = + let isUnknown option = flags |> Map.values |> Seq.contains option |> not + options |> Seq.exists isUnknown match optionPairs with | o when o.Keys |> hasMalformedOption -> Error MalformedFlags - | o when o.Keys |> hasUnsupportedOption -> Error UnsupportedFlags + | o when o.Keys |> hasUnknownOption -> Error UnknownFlags | _ -> Ok () let validate args = @@ -61,14 +53,14 @@ module ArgValidation = let! count = FileCount.Create fileCountArg - let! optionArgPairs = optionArgs |> toPairs + let! optionArgPairs = toPairs optionArgs do! verifyOptionArgs optionArgPairs - let tryGetArg x = optionArgPairs |> Map.tryFind flags[x] + let tryGetArg x = Map.tryFind flags[x] optionArgPairs - let p = Prefix.Create (tryGetArg Prefix) + let p = Prefix.Create (tryGetArg Prefix) let! b = NameBaseLength.TryCreate (tryGetArg NameBaseLength) - let e = Extension.Create (tryGetArg Extension) - let o = OutputDirectory.Create (tryGetArg OutputDirectory) + let e = Extension.Create (tryGetArg Extension) + let o = OutputDirectory.Create (tryGetArg OutputDirectory) let! s = Size.TryCreate (tryGetArg Size) let! d = Delay.TryCreate (tryGetArg Delay) diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index 3eb216a..c6bc907 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -7,7 +7,7 @@ module Errors = | NoArgsPassed | ArgCountInvalid | MalformedFlags - | UnsupportedFlags + | UnknownFlags | DuplicateFlags | ParseNumberFailure of Arg: string * AllowedRange: (int * int) | DirectoryMissing of string @@ -21,7 +21,7 @@ module Errors = | NoArgsPassed -> "You must pass in at least one argument: the number of files to generate." | ArgCountInvalid -> "Invalid argument count." | MalformedFlags -> "Malformed flag(s) found." - | UnsupportedFlags -> "Unsupported flag(s) found." + | UnknownFlags -> "Unknown flag(s) found." | DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once." | ParseNumberFailure (x, (f, c)) -> $"Could not parse \"%s{x}\" to an integer between %s{formatInt f} and %s{formatInt c}, inclusive." diff --git a/src/UniqueFileGenerator.Console/Io.fs b/src/UniqueFileGenerator.Console/Io.fs index b5f71e9..d2cc370 100644 --- a/src/UniqueFileGenerator.Console/Io.fs +++ b/src/UniqueFileGenerator.Console/Io.fs @@ -8,24 +8,25 @@ open Utilities open System open System.IO open System.Threading +open CCFSharpUtils.Text module Io = let verifyDirectory dir = - match Directory.Exists dir with - | true -> Ok () - | false -> Error (DirectoryMissing dir) + if Directory.Exists dir + then Ok () + else Error (DirectoryMissing dir) - let private formatBytes (bytes: int64) = - let kilobyte = 1024L - let megabyte = kilobyte * 1024L - let gigabyte = megabyte * 1024L - let terabyte = gigabyte * 1024L + let private formatBytes bytes = + let kb = 1024L + let mb = kb * 1024L + let gb = mb * 1024L + let tb = gb * 1024L match bytes with - | _ when bytes >= terabyte -> sprintf "%s TB" ((float bytes / float terabyte) |> formatFloat) - | _ when bytes >= gigabyte -> sprintf "%s GB" ((float bytes / float gigabyte) |> formatFloat) - | _ when bytes >= megabyte -> sprintf "%s MB" ((float bytes / float megabyte) |> formatFloat) - | _ when bytes >= kilobyte -> sprintf "%s KB" ((float bytes / float kilobyte) |> formatFloat) + | _ when bytes >= tb -> sprintf "%s TB" ((float bytes / float tb) |> formatFloat) + | _ when bytes >= gb -> sprintf "%s GB" ((float bytes / float gb) |> formatFloat) + | _ when bytes >= mb -> sprintf "%s MB" ((float bytes / float mb) |> formatFloat) + | _ when bytes >= kb -> sprintf "%s KB" ((float bytes / float kb) |> formatFloat) | _ -> sprintf "%s bytes" (bytes |> formatInt64) let verifyDriveSpace (args: Args) = @@ -41,6 +42,7 @@ module Io = let confirmContinueDespiteLargeSize availableSpace : bool = let ratio = float neededSpace / float availableSpace let isLargeRatio = ratio > warningRatio + let yesAnswers = [| "y"; "yes" |] let confirm () = Console.Write( @@ -50,8 +52,7 @@ module Io = let reply = Console.ReadLine().Trim() - [| "y"; "yes" |] - |> Array.exists (fun yesAnswer -> reply.Equals(yesAnswer, StringComparison.InvariantCultureIgnoreCase)) + Array.exists (String.equalIgnoreCase reply) yesAnswers if isLargeRatio then confirm () diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index 10a324f..72e5da7 100644 --- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs +++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs @@ -101,7 +101,7 @@ let ``Appropriate error when unsupported symbol flag found`` () = let ``Appropriate error when unsupported flag(s) found`` () = let unsupportedFlag = "-a" let args = [| validFileCountArg; unsupportedFlag; "0" |] - let expected = Error UnsupportedFlags + let expected = Error UnknownFlags let actual = validate args Assert.Equal(expected, actual) From 23b1697d45318d09d35e92a21c70cda53b053015 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:42:59 +0900 Subject: [PATCH 13/49] Remove superfluous sleep function --- src/UniqueFileGenerator.Console/Io.fs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Io.fs b/src/UniqueFileGenerator.Console/Io.fs index d2cc370..fcc2427 100644 --- a/src/UniqueFileGenerator.Console/Io.fs +++ b/src/UniqueFileGenerator.Console/Io.fs @@ -8,6 +8,7 @@ open Utilities open System open System.IO open System.Threading +open CCFSharpUtils.Operators open CCFSharpUtils.Text module Io = @@ -84,7 +85,7 @@ module Io = with | e -> Error $"%s{e.Message}" - let generateFiles (args: Args) = + let generateFiles (args: Args) : unit = let count, prefix, baseLength, ext, outputDir, size, delay = args.FileCount, args.Options.Prefix, @@ -97,15 +98,11 @@ module Io = let generateFileName baseName = toFileName { Prefix = prefix; Base = baseName; Ext = ext } - let sleep (ms: int) x = - Thread.Sleep ms - x - let writeFile fileName = fileName |> generateFileContent size |> createFile outputDir fileName - |> sleep delay + |-- (fun _ -> Thread.Sleep delay) |> printResult generateMultiple baseLength count From 97c6028222f2557ab39c209c8d8c506b800920df Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:56:41 +0900 Subject: [PATCH 14/49] Use map and iter; etc. --- src/UniqueFileGenerator.Console/Io.fs | 9 +++++---- src/UniqueFileGenerator.Console/StringGeneration.fs | 2 +- src/UniqueFileGenerator.Tests/StringGenerationTests.fs | 9 +++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Io.fs b/src/UniqueFileGenerator.Console/Io.fs index fcc2427..64d4386 100644 --- a/src/UniqueFileGenerator.Console/Io.fs +++ b/src/UniqueFileGenerator.Console/Io.fs @@ -8,6 +8,7 @@ open Utilities open System open System.IO open System.Threading +open FSharpPlus.Operators open CCFSharpUtils.Operators open CCFSharpUtils.Text @@ -77,7 +78,7 @@ module Io = with | e -> Error (IoError $"%s{e.Message}") - let private createFile directory fileName (contents: string) = + let private writeFile directory fileName (contents: string) = try let path = Path.Combine(directory, fileName) File.WriteAllText(path, contents) @@ -101,10 +102,10 @@ module Io = let writeFile fileName = fileName |> generateFileContent size - |> createFile outputDir fileName + |> writeFile outputDir fileName |-- (fun _ -> Thread.Sleep delay) |> printResult generateMultiple baseLength count - |> Array.map generateFileName - |> Array.iter writeFile + |> map generateFileName + |> iter writeFile diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index aaffd36..9a056c2 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -12,7 +12,7 @@ module StringGeneration = let private charBank = [ 'A' .. 'Z' ] @ [ 'a' .. 'z' ] @ [ '0' .. '9' ] - |> List.map string + |> map string |> String.concat String.Empty let private rnd = Random() diff --git a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs index 75800f6..8ee01a5 100644 --- a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs +++ b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs @@ -3,6 +3,7 @@ module StringGenerationTests open UniqueFileGenerator.Console.StringGeneration open System open Xunit +open FSharpPlus.Operators module Strings = [] @@ -50,7 +51,7 @@ module FileNames = let fileNames = generated - |> Array.map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) + |> map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) Assert.Equal(generated, fileNames) @@ -62,7 +63,7 @@ module FileNames = let fileNames = generated - |> Array.map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) + |> map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) Assert.True(fileNames |> Array.forall (fun x -> x.StartsWith prefix)) @@ -74,7 +75,7 @@ module FileNames = let fileNames = generated - |> Array.map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) + |> map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) Assert.True(fileNames |> Array.forall (fun x -> x.EndsWith extension)) @@ -86,7 +87,7 @@ module FileNames = let fileNames = generated - |> Array.map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) + |> map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) Assert.True(fileNames |> Array.forall (fun x -> x.StartsWith prefix)) Assert.True(fileNames |> Array.forall (fun x -> x.EndsWith extension)) From f8a0b43d340b1858d4acba4a68b9c27e587a419d Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:14:36 +0900 Subject: [PATCH 15/49] Rename OptionType to AppOption --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 4 ++-- src/UniqueFileGenerator.Console/Printing.fs | 2 +- src/UniqueFileGenerator.Tests/ArgParserTests.fs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index a6b89a0..53fe5a7 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -103,7 +103,7 @@ module ArgTypes = member this.Value = let (Delay length) = this in length - type OptionType = + type AppOption = | Prefix | NameBaseLength | Extension @@ -137,7 +137,7 @@ module ArgTypes = Size = options.Size Delay = options.Delay } } - let flags: Map = + let flags: Map = [ Prefix, "-p" NameBaseLength, "-b" Extension, "-e" diff --git a/src/UniqueFileGenerator.Console/Printing.fs b/src/UniqueFileGenerator.Console/Printing.fs index 5d60a97..9c8f9ed 100644 --- a/src/UniqueFileGenerator.Console/Printing.fs +++ b/src/UniqueFileGenerator.Console/Printing.fs @@ -23,5 +23,5 @@ module Printing = printLineColor (Some ConsoleColor.Red) msg let printResult = function - | Ok x -> printLine $"OK: %s{x}" + | Ok x -> printLine $"OK: %s{x}" | Error e -> printError $"Error: %s{e}" diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index 72e5da7..4ccd0aa 100644 --- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs +++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs @@ -9,7 +9,7 @@ open Xunit let validFileCountArg = "1000" let validOptionValues = - Map.ofList + Map.ofList [ Prefix, "PREFIX " NameBaseLength, "60" Extension, ".txt" From 9ce259bceed9d30aa5d07815d48b15194eaca367 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:19:17 +0900 Subject: [PATCH 16/49] Rename ErrorType to AppError --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 2 +- src/UniqueFileGenerator.Console/Errors.fs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 53fe5a7..4a1df53 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -19,7 +19,7 @@ module ArgTypes = type FileCount = private FileCount of int with static member val AllowedRange = 1, Int32.MaxValue - static member Create text : Result = + static member Create text : Result = text |> stripSeparators |> parseInRange FileCount.AllowedRange diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index c6bc907..899414b 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -3,7 +3,8 @@ namespace UniqueFileGenerator.Console open Utilities module Errors = - type ErrorType = + + type AppError = | NoArgsPassed | ArgCountInvalid | MalformedFlags From 3bb87b12e918ef02fb25f0d2ba220df392fa68db Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:21:31 +0900 Subject: [PATCH 17/49] Rename getMessage to errorMsg and make minor tweaks --- src/UniqueFileGenerator.Console/Errors.fs | 10 +++++----- src/UniqueFileGenerator.Console/Program.fs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index 899414b..8f7b777 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -17,19 +17,19 @@ module Errors = | IoError of string | CancelledByUser - let getMessage error = + let errorMsg error = match error with | NoArgsPassed -> "You must pass in at least one argument: the number of files to generate." | ArgCountInvalid -> "Invalid argument count." | MalformedFlags -> "Malformed flag(s) found." | UnknownFlags -> "Unknown flag(s) found." | DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once." - | ParseNumberFailure (x, (f, c)) -> - $"Could not parse \"%s{x}\" to an integer between %s{formatInt f} and %s{formatInt c}, inclusive." - | DirectoryMissing e -> $"Directory \"%s{e}\" was not found." + | ParseNumberFailure (input, (floor, ceiling)) -> + $"Cannot parse \"%s{input}\" to an integer between %s{formatInt floor} and %s{formatInt ceiling}, inclusive." + | DirectoryMissing dirName -> $"Directory \"%s{dirName}\" was not found." | DriveSpaceConfirmationFailure -> "Could not confirm available drive space." | DriveSpaceInsufficient (needed, actual) -> $"Insufficient drive space. Though %s{needed} is necessary, only %s{actual} is available." - | IoError e -> $"IO error: %s{e}" + | IoError msg -> $"IO error: %s{msg}" | CancelledByUser -> "Cancelled." diff --git a/src/UniqueFileGenerator.Console/Program.fs b/src/UniqueFileGenerator.Console/Program.fs index d132bc2..d86c439 100644 --- a/src/UniqueFileGenerator.Console/Program.fs +++ b/src/UniqueFileGenerator.Console/Program.fs @@ -35,7 +35,7 @@ module Main = printLine $"Done after %s{watch.ElapsedFriendly}. Used approximately %s{spaceUsed} of drive space." ExitCode.Success | Error e -> - printError <| getMessage e + printError <| errorMsg e Help.suggest () ExitCode.Error |> int From 375a7b812d5244f2a9735a130f63fc7d75399b30 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:16:02 +0900 Subject: [PATCH 18/49] Improve matching --- .../Arguments/ArgumentValidation.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index e57109f..b5b5f8e 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -41,9 +41,9 @@ module ArgValidation = let isUnknown option = flags |> Map.values |> Seq.contains option |> not options |> Seq.exists isUnknown - match optionPairs with - | o when o.Keys |> hasMalformedOption -> Error MalformedFlags - | o when o.Keys |> hasUnknownOption -> Error UnknownFlags + match optionPairs.Keys with + | keys when hasMalformedOption keys -> Error MalformedFlags + | keys when hasUnknownOption keys -> Error UnknownFlags | _ -> Ok () let validate args = From 1985de883cb130ecd6af9aaa3896515a25499eaa Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:18:17 +0900 Subject: [PATCH 19/49] Nomenclature --- .../Arguments/ArgumentValidation.fs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index b5b5f8e..1e2296f 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -8,19 +8,19 @@ open FsToolkit.ErrorHandling open ArgTypes module ArgValidation = - let private verifyArgCount (args: string array) = + let private validateArgCount (args: string array) = match args.Length with | 0 -> Error NoArgsPassed | l when Num.isEven l -> Error ArgCountInvalid | _ -> Ok () - let private toPairs (argPairs: string array) = + let private toPairs (args: string array) = let hasDuplicate xs = let originalLength = Seq.length xs let uniqueLength = xs |> Set.ofSeq |> Set.count originalLength <> uniqueLength - argPairs + args |> Array.chunkBySize 2 // Will throw if array length is odd! |> Array.map (fun x -> x[0].ToLowerInvariant(), x[1]) |> fun pairs -> @@ -28,7 +28,7 @@ module ArgValidation = then Error DuplicateFlags else Ok (Map.ofArray pairs) - let private verifyOptionArgs (optionPairs: Map) = + let private validateOptionArgs (optionPairs: Map) = let hasMalformedOption optionPairs = let isCorrectFormat (o: string) = o.Length = 2 && o.StartsWith "-" && Char.IsLetter o[1] @@ -37,9 +37,9 @@ module ArgValidation = |> Seq.forall isCorrectFormat |> not - let hasUnknownOption options = - let isUnknown option = flags |> Map.values |> Seq.contains option |> not - options |> Seq.exists isUnknown + let hasUnknownOption appOptions = + let isUnknown appOption = flags |> Map.values |> Seq.contains appOption |> not + appOptions |> Seq.exists isUnknown match optionPairs.Keys with | keys when hasMalformedOption keys -> Error MalformedFlags @@ -48,15 +48,15 @@ module ArgValidation = let validate args = result { - do! verifyArgCount args + do! validateArgCount args let fileCountArg, optionArgs = args[0], args[1..] - let! count = FileCount.Create fileCountArg + let! fileCount = FileCount.Create fileCountArg let! optionArgPairs = toPairs optionArgs - do! verifyOptionArgs optionArgPairs - let tryGetArg x = Map.tryFind flags[x] optionArgPairs + do! validateOptionArgs optionArgPairs + let tryGetArg x = Map.tryFind flags[x] optionArgPairs let p = Prefix.Create (tryGetArg Prefix) let! b = NameBaseLength.TryCreate (tryGetArg NameBaseLength) let e = Extension.Create (tryGetArg Extension) @@ -72,5 +72,5 @@ module ArgValidation = Size = s.Value Delay = d.Value } - return Args.Create(count, options) + return Args.Create(fileCount, options) } From b470de56a8cf26a3ab148e9da37c547b4ee9c997 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:31:02 +0900 Subject: [PATCH 20/49] Use Array.head and .tail; etc. --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 1 + .../Arguments/ArgumentValidation.fs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 4a1df53..d55fd61 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -7,6 +7,7 @@ open FSharpPlus open CCFSharpUtils.Text module ArgTypes = + let supportedSeparators = [ ","; "_" ] let stripSeparators = String.stripSubstrings supportedSeparators diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index 1e2296f..61fe571 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -21,8 +21,8 @@ module ArgValidation = originalLength <> uniqueLength args - |> Array.chunkBySize 2 // Will throw if array length is odd! - |> Array.map (fun x -> x[0].ToLowerInvariant(), x[1]) + |> Array.chunkBySize 2 + |> Array.map (fun pair -> pair[0].ToLowerInvariant(), pair[1]) |> fun pairs -> if pairs |> Array.map fst |> hasDuplicate then Error DuplicateFlags @@ -49,7 +49,7 @@ module ArgValidation = let validate args = result { do! validateArgCount args - let fileCountArg, optionArgs = args[0], args[1..] + let fileCountArg, optionArgs = Array.head args, Array.tail args let! fileCount = FileCount.Create fileCountArg From 7abbce25aaf643bca93d71170859a2470125ff04 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:37:36 +0900 Subject: [PATCH 21/49] Remove unneeded variables --- .../Arguments/ArgumentValidation.fs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index 61fe571..d581902 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -49,11 +49,10 @@ module ArgValidation = let validate args = result { do! validateArgCount args - let fileCountArg, optionArgs = Array.head args, Array.tail args - let! fileCount = FileCount.Create fileCountArg + let! fileCount = FileCount.Create <| Array.head args - let! optionArgPairs = toPairs optionArgs + let! optionArgPairs = toPairs <| Array.tail args do! validateOptionArgs optionArgPairs let tryGetArg x = Map.tryFind flags[x] optionArgPairs From 5033dacb45534365cd709ef7856e1be099d327ac Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:28:39 +0900 Subject: [PATCH 22/49] Refactor ArgumentValidation --- .../Arguments/ArgumentValidation.fs | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index d581902..7be7073 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -1,19 +1,37 @@ namespace UniqueFileGenerator.Console -open UniqueFileGenerator.Console -open System +open ArgTypes open Errors +open System open CCFSharpUtils open FsToolkit.ErrorHandling -open ArgTypes module ArgValidation = + /// Ensure the count of args is odd, which is currently the only valid shape. let private validateArgCount (args: string array) = match args.Length with | 0 -> Error NoArgsPassed | l when Num.isEven l -> Error ArgCountInvalid | _ -> Ok () + let private validateOptionArgs (optionMap: Map) = + let hasMalformedOptionKey keys = + let isCorrectFormat (o: string) = + o.Length = 2 && o.StartsWith "-" && Char.IsLetter o[1] + + keys + |> Seq.forall isCorrectFormat + |> not + + let hasUnknownOptionKey appOptions = + let isUnknown appOption = flags |> Map.values |> Seq.contains appOption |> not + appOptions |> Seq.exists isUnknown + + match optionMap.Keys with + | keys when hasMalformedOptionKey keys -> Error MalformedFlags + | keys when hasUnknownOptionKey keys -> Error UnknownFlags + | _ -> Ok optionMap + let private toPairs (args: string array) = let hasDuplicate xs = let originalLength = Seq.length xs @@ -24,38 +42,21 @@ module ArgValidation = |> Array.chunkBySize 2 |> Array.map (fun pair -> pair[0].ToLowerInvariant(), pair[1]) |> fun pairs -> - if pairs |> Array.map fst |> hasDuplicate + // Check for duplicates here because conversion to a map will + // silently use only the last duplicate (though that is apparently + // undocumented behavior with a chance of changing in the future). + if hasDuplicate (Array.map fst pairs) then Error DuplicateFlags - else Ok (Map.ofArray pairs) - - let private validateOptionArgs (optionPairs: Map) = - let hasMalformedOption optionPairs = - let isCorrectFormat (o: string) = - o.Length = 2 && o.StartsWith "-" && Char.IsLetter o[1] - - optionPairs - |> Seq.forall isCorrectFormat - |> not - - let hasUnknownOption appOptions = - let isUnknown appOption = flags |> Map.values |> Seq.contains appOption |> not - appOptions |> Seq.exists isUnknown - - match optionPairs.Keys with - | keys when hasMalformedOption keys -> Error MalformedFlags - | keys when hasUnknownOption keys -> Error UnknownFlags - | _ -> Ok () + else validateOptionArgs (Map.ofArray pairs) let validate args = result { do! validateArgCount args - let! fileCount = FileCount.Create <| Array.head args - - let! optionArgPairs = toPairs <| Array.tail args - do! validateOptionArgs optionArgPairs + let! fileCount = FileCount.Create (Array.head args) + let! optionMap = toPairs (Array.tail args) - let tryGetArg x = Map.tryFind flags[x] optionArgPairs + let tryGetArg x = Map.tryFind flags[x] optionMap let p = Prefix.Create (tryGetArg Prefix) let! b = NameBaseLength.TryCreate (tryGetArg NameBaseLength) let e = Extension.Create (tryGetArg Extension) From da0588f1b44d8d488de5660a7d58f848d0244e52 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:52:25 +0900 Subject: [PATCH 23/49] Use Result.bimap --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 5 +++-- .../Arguments/ArgumentValidation.fs | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index d55fd61..0b17c64 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -24,8 +24,9 @@ module ArgTypes = text |> stripSeparators |> parseInRange FileCount.AllowedRange - |> Result.map FileCount - |> Result.mapError (fun _ -> ParseNumberFailure (text, FileCount.AllowedRange)) + |> Result.bimap + (fun _ -> ParseNumberFailure (text, FileCount.AllowedRange)) + FileCount member this.Value = let (FileCount count) = this in count diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index 7be7073..d5c30d2 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -19,9 +19,7 @@ module ArgValidation = let isCorrectFormat (o: string) = o.Length = 2 && o.StartsWith "-" && Char.IsLetter o[1] - keys - |> Seq.forall isCorrectFormat - |> not + keys |> Seq.forall isCorrectFormat |> not let hasUnknownOptionKey appOptions = let isUnknown appOption = flags |> Map.values |> Seq.contains appOption |> not From 376fdd57ad1faa27640402b4cb30f30287a6fdb5 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:58:46 +0900 Subject: [PATCH 24/49] Rename function --- src/UniqueFileGenerator.Console/Help.fs | 2 +- src/UniqueFileGenerator.Console/Program.fs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Help.fs b/src/UniqueFileGenerator.Console/Help.fs index 7826cde..a9731e4 100644 --- a/src/UniqueFileGenerator.Console/Help.fs +++ b/src/UniqueFileGenerator.Console/Help.fs @@ -8,7 +8,7 @@ open Printing module Help = let private helpFlag = "--help" - let wasRequested (args: string array) = + let isRequested (args: string array) = args.Length > 0 && args[0].Trim().Equals(helpFlag, StringComparison.InvariantCultureIgnoreCase) diff --git a/src/UniqueFileGenerator.Console/Program.fs b/src/UniqueFileGenerator.Console/Program.fs index d86c439..2155d43 100644 --- a/src/UniqueFileGenerator.Console/Program.fs +++ b/src/UniqueFileGenerator.Console/Program.fs @@ -8,6 +8,7 @@ open Io open FsToolkit.ErrorHandling module Main = + type ExitCode = | Success = 0 | Error = 1 @@ -16,7 +17,7 @@ module Main = let main rawArgs = let watch = Startwatch.Library.Watch() - let run (rawArgs: string array) = + let run rawArgs = result { let! args = validate rawArgs do! verifyDirectory args.Options.OutputDirectory @@ -26,7 +27,7 @@ module Main = return spaceNeeded } - if Help.wasRequested rawArgs then + if Help.isRequested rawArgs then Help.print () ExitCode.Success else From ed187dcfe1e84cda7ca56b70b9b376bcee56372b Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:08:59 +0900 Subject: [PATCH 25/49] Rename function to tryParseInRange --- .../Arguments/ArgumentTypes.fs | 4 ++-- src/UniqueFileGenerator.Console/Utilities.fs | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 0b17c64..1ad8184 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -14,7 +14,7 @@ module ArgTypes = let private tryParseIntInRange (floor, ceiling) text = text - |> parseInRange (floor, ceiling) + |> tryParseInRange (floor, ceiling) |> Result.mapError (fun _ -> ParseNumberFailure (text, (floor, ceiling))) type FileCount = private FileCount of int with @@ -23,7 +23,7 @@ module ArgTypes = static member Create text : Result = text |> stripSeparators - |> parseInRange FileCount.AllowedRange + |> tryParseInRange FileCount.AllowedRange |> Result.bimap (fun _ -> ParseNumberFailure (text, FileCount.AllowedRange)) FileCount diff --git a/src/UniqueFileGenerator.Console/Utilities.fs b/src/UniqueFileGenerator.Console/Utilities.fs index a9f4cbe..b6f2a40 100644 --- a/src/UniqueFileGenerator.Console/Utilities.fs +++ b/src/UniqueFileGenerator.Console/Utilities.fs @@ -13,18 +13,17 @@ module Utilities = let formatFloat (f: float) : string = f.ToString("#,##0.00", CultureInfo.InvariantCulture) + /// Checks if a value falls within an inclusive range. let inline (>=<) x (floor, ceiling) = x >= floor && x <= ceiling let tryParseInt (input: string) : int option = match Int32.TryParse input with - | true, i -> Some i + | true, i -> Some i | false, _ -> None - let parseInRange (floor, ceiling) (x: string) : Result = - match tryParseInt x with - | Some i when (>=<) i (floor, ceiling) -> - Ok i - | _ -> - Error () + let tryParseInRange (floor, ceiling) (text: string) : Result = + match tryParseInt text with + | Some i when (>=<) i (floor, ceiling) -> Ok i + | _ -> Error () From 31211bb3d276c2f310370153b96f932af9fc6d39 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:16:53 +0900 Subject: [PATCH 26/49] Replace 3 functions with overloaded methods --- src/UniqueFileGenerator.Console/Errors.fs | 2 +- src/UniqueFileGenerator.Console/Io.fs | 12 ++++++------ src/UniqueFileGenerator.Console/Utilities.fs | 18 ++++++++++-------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index 8f7b777..4ca49fa 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -25,7 +25,7 @@ module Errors = | UnknownFlags -> "Unknown flag(s) found." | DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once." | ParseNumberFailure (input, (floor, ceiling)) -> - $"Cannot parse \"%s{input}\" to an integer between %s{formatInt floor} and %s{formatInt ceiling}, inclusive." + $"Cannot parse \"%s{input}\" to an integer between %s{Num.Format floor} and %s{Num.Format ceiling}, inclusive." | DirectoryMissing dirName -> $"Directory \"%s{dirName}\" was not found." | DriveSpaceConfirmationFailure -> "Could not confirm available drive space." | DriveSpaceInsufficient (needed, actual) -> diff --git a/src/UniqueFileGenerator.Console/Io.fs b/src/UniqueFileGenerator.Console/Io.fs index 64d4386..6b4c702 100644 --- a/src/UniqueFileGenerator.Console/Io.fs +++ b/src/UniqueFileGenerator.Console/Io.fs @@ -25,11 +25,11 @@ module Io = let tb = gb * 1024L match bytes with - | _ when bytes >= tb -> sprintf "%s TB" ((float bytes / float tb) |> formatFloat) - | _ when bytes >= gb -> sprintf "%s GB" ((float bytes / float gb) |> formatFloat) - | _ when bytes >= mb -> sprintf "%s MB" ((float bytes / float mb) |> formatFloat) - | _ when bytes >= kb -> sprintf "%s KB" ((float bytes / float kb) |> formatFloat) - | _ -> sprintf "%s bytes" (bytes |> formatInt64) + | _ when bytes >= tb -> sprintf "%s TB" ((float bytes / float tb) |> Num.Format) + | _ when bytes >= gb -> sprintf "%s GB" ((float bytes / float gb) |> Num.Format) + | _ when bytes >= mb -> sprintf "%s MB" ((float bytes / float mb) |> Num.Format) + | _ when bytes >= kb -> sprintf "%s KB" ((float bytes / float kb) |> Num.Format) + | _ -> sprintf "%s bytes" (bytes |> Num.Format) let verifyDriveSpace (args: Args) = let driveSpaceToKeepAvailable = 536_870_912L // 0.5 GB @@ -50,7 +50,7 @@ module Io = Console.Write( sprintf "This operation requires %s, which is %s%% of remaining drive space. Continue? (Y/n) " (neededSpace |> formatBytes) - (ratio * 100.0 |> formatFloat)) + (ratio * 100.0 |> Num.Format)) let reply = Console.ReadLine().Trim() diff --git a/src/UniqueFileGenerator.Console/Utilities.fs b/src/UniqueFileGenerator.Console/Utilities.fs index b6f2a40..2ebf359 100644 --- a/src/UniqueFileGenerator.Console/Utilities.fs +++ b/src/UniqueFileGenerator.Console/Utilities.fs @@ -4,14 +4,6 @@ open System open System.Globalization module Utilities = - let formatInt (i: int) : string = - i.ToString("#,##0", CultureInfo.InvariantCulture) - - let formatInt64 (i: int64) : string = - i.ToString("#,##0", CultureInfo.InvariantCulture) - - let formatFloat (f: float) : string = - f.ToString("#,##0.00", CultureInfo.InvariantCulture) /// Checks if a value falls within an inclusive range. let inline (>=<) x (floor, ceiling) = x >= floor && x <= ceiling @@ -26,4 +18,14 @@ module Utilities = | Some i when (>=<) i (floor, ceiling) -> Ok i | _ -> Error () + // Numeric operations. + type Num = + + static member Format(i: int) : string = + i.ToString("#,##0", CultureInfo.InvariantCulture) + + static member Format(i: int64) : string = + i.ToString("#,##0", CultureInfo.InvariantCulture) + static member Format(f: float) : string = + f.ToString("#,##0.00", CultureInfo.InvariantCulture) From 916a30f4bb7b0115df643067968c14248c545435 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:21:44 +0900 Subject: [PATCH 27/49] Move Num type culture to static let --- src/UniqueFileGenerator.Console/Utilities.fs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Utilities.fs b/src/UniqueFileGenerator.Console/Utilities.fs index 2ebf359..cece958 100644 --- a/src/UniqueFileGenerator.Console/Utilities.fs +++ b/src/UniqueFileGenerator.Console/Utilities.fs @@ -21,11 +21,13 @@ module Utilities = // Numeric operations. type Num = + static let culture = CultureInfo.InvariantCulture + static member Format(i: int) : string = - i.ToString("#,##0", CultureInfo.InvariantCulture) + i.ToString("#,##0", culture) static member Format(i: int64) : string = - i.ToString("#,##0", CultureInfo.InvariantCulture) + i.ToString("#,##0", culture) static member Format(f: float) : string = - f.ToString("#,##0.00", CultureInfo.InvariantCulture) + f.ToString("#,##0.00", culture) From 35c0cd0e579d5bbff7318738e7630ca39a78af55 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:23:06 +0900 Subject: [PATCH 28/49] Code layout --- src/UniqueFileGenerator.Console/Utilities.fs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Utilities.fs b/src/UniqueFileGenerator.Console/Utilities.fs index cece958..39c5fd3 100644 --- a/src/UniqueFileGenerator.Console/Utilities.fs +++ b/src/UniqueFileGenerator.Console/Utilities.fs @@ -20,14 +20,7 @@ module Utilities = // Numeric operations. type Num = - static let culture = CultureInfo.InvariantCulture - - static member Format(i: int) : string = - i.ToString("#,##0", culture) - - static member Format(i: int64) : string = - i.ToString("#,##0", culture) - - static member Format(f: float) : string = - f.ToString("#,##0.00", culture) + static member Format(i: int) : string = i.ToString("#,##0", culture) + static member Format(i: int64) : string = i.ToString("#,##0", culture) + static member Format(f: float) : string = f.ToString("#,##0.00", culture) From 8cab1a823b22ee8f98d7264ab0288571c0471db0 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:07:16 +0900 Subject: [PATCH 29/49] Rename text to maybeText --- .../Arguments/ArgumentTypes.fs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 1ad8184..d188c0c 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -33,8 +33,8 @@ module ArgTypes = type Prefix = private Prefix of string with static member val Default = String.Empty - static member Create text = - text + static member Create maybeText = + maybeText |> option id Prefix.Default |> Prefix @@ -44,8 +44,8 @@ module ArgTypes = static member val AllowedRange = 1, 100 static member val Default = 50 - static member TryCreate text = - text + static member TryCreate maybeText = + maybeText |> option (stripSeparators >> String.trim @@ -58,8 +58,8 @@ module ArgTypes = type Extension = private Extension of string with static member val Default = String.Empty - static member Create text = - text + static member Create maybeText = + maybeText |> option String.trim Extension.Default |> Extension @@ -68,8 +68,8 @@ module ArgTypes = type OutputDirectory = private OutputDirectory of string with static member val Default = "output" - static member Create text = - text + static member Create maybeText = + maybeText |> option String.trim OutputDirectory.Default |> OutputDirectory @@ -78,8 +78,8 @@ module ArgTypes = type Size = private Size of int option with static member val AllowedRange = 1, Int32.MaxValue - static member TryCreate text = - text + static member TryCreate maybeText = + maybeText |> Option.map (stripSeparators >> String.trim >> tryParseIntInRange Size.AllowedRange) @@ -94,8 +94,8 @@ module ArgTypes = static member val AllowedRange = 0, Int32.MaxValue static member val Default = 0 - static member TryCreate text = - text + static member TryCreate maybeText = + maybeText |> option (stripSeparators >> String.trim From 72efee535c7041c53d238b760afbb498be6855e3 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:12:30 +0900 Subject: [PATCH 30/49] Remove parens --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index d188c0c..803e82d 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -149,4 +149,4 @@ module ArgTypes = |> Map.ofList let fileNameLength options = - (options.Prefix.Length + options.NameBaseLength + options.Extension.Length) + options.Prefix.Length + options.NameBaseLength + options.Extension.Length From 991394f020b9f8ccd9b839fc746f65a8b4ccbec8 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:08:27 +0900 Subject: [PATCH 31/49] Rename errors; tweak error messages --- .../Arguments/ArgumentTypes.fs | 4 ++-- .../Arguments/ArgumentValidation.fs | 2 +- src/UniqueFileGenerator.Console/Errors.fs | 16 ++++++++-------- src/UniqueFileGenerator.Tests/ArgParserTests.fs | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 1ad8184..f8d15e4 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -15,7 +15,7 @@ module ArgTypes = let private tryParseIntInRange (floor, ceiling) text = text |> tryParseInRange (floor, ceiling) - |> Result.mapError (fun _ -> ParseNumberFailure (text, (floor, ceiling))) + |> Result.mapError (fun _ -> NumberParseFailure (text, (floor, ceiling))) type FileCount = private FileCount of int with static member val AllowedRange = 1, Int32.MaxValue @@ -25,7 +25,7 @@ module ArgTypes = |> stripSeparators |> tryParseInRange FileCount.AllowedRange |> Result.bimap - (fun _ -> ParseNumberFailure (text, FileCount.AllowedRange)) + (fun _ -> NumberParseFailure (text, FileCount.AllowedRange)) FileCount member this.Value = let (FileCount count) = this in count diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index d5c30d2..9b48bf9 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -10,7 +10,7 @@ module ArgValidation = /// Ensure the count of args is odd, which is currently the only valid shape. let private validateArgCount (args: string array) = match args.Length with - | 0 -> Error NoArgsPassed + | 0 -> Error ArgsMissing | l when Num.isEven l -> Error ArgCountInvalid | _ -> Ok () diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index 4ca49fa..0f80226 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -5,12 +5,12 @@ open Utilities module Errors = type AppError = - | NoArgsPassed + | ArgsMissing | ArgCountInvalid | MalformedFlags | UnknownFlags | DuplicateFlags - | ParseNumberFailure of Arg: string * AllowedRange: (int * int) + | NumberParseFailure of Input: string * AllowedRange: (int * int) | DirectoryMissing of string | DriveSpaceConfirmationFailure | DriveSpaceInsufficient of Needed: string * Actual: string @@ -19,17 +19,17 @@ module Errors = let errorMsg error = match error with - | NoArgsPassed -> "You must pass in at least one argument: the number of files to generate." - | ArgCountInvalid -> "Invalid argument count." - | MalformedFlags -> "Malformed flag(s) found." - | UnknownFlags -> "Unknown flag(s) found." + | ArgsMissing -> "You must pass in at least one argument: the number of files to generate." + | ArgCountInvalid -> "Invalid arguments. If you submit option flags, each must have a corresponding value." + | MalformedFlags -> "Malformed option flag(s) found." + | UnknownFlags -> "Unknown option flag(s) found." | DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once." - | ParseNumberFailure (input, (floor, ceiling)) -> + | NumberParseFailure (input, (floor, ceiling)) -> $"Cannot parse \"%s{input}\" to an integer between %s{Num.Format floor} and %s{Num.Format ceiling}, inclusive." | DirectoryMissing dirName -> $"Directory \"%s{dirName}\" was not found." | DriveSpaceConfirmationFailure -> "Could not confirm available drive space." | DriveSpaceInsufficient (needed, actual) -> $"Insufficient drive space. Though %s{needed} is necessary, only %s{actual} is available." | IoError msg -> $"IO error: %s{msg}" - | CancelledByUser -> "Cancelled." + | CancelledByUser -> "Cancelled by the user." diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index 4ccd0aa..e2a54c7 100644 --- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs +++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs @@ -43,7 +43,7 @@ let createOkArgs fileCount options = [] let ``Appropriate error when no args`` () = let emptyArgs = [||] - let expected = Error NoArgsPassed + let expected = Error ArgsMissing let actual = validate emptyArgs Assert.Equal(expected, actual) @@ -64,21 +64,21 @@ let ``Appropriate error when invalid arg count (second pair incomplete)`` () = [] let ``Appropriate error when invalid file count`` () = let args = [| "notNumeric" |] - let expected = Error (ParseNumberFailure(args[0], (1, Int32.MaxValue))) + let expected = Error (NumberParseFailure(args[0], (1, Int32.MaxValue))) let actual = validate args Assert.Equal(expected, actual) [] let ``Appropriate error when negative file count`` () = let args = [| "-1" |] - let expected = Error (ParseNumberFailure(args[0], (1, Int32.MaxValue))) + let expected = Error (NumberParseFailure(args[0], (1, Int32.MaxValue))) let actual = validate args Assert.Equal(expected, actual) [] let ``Appropriate error when zero file count`` () = let args = [| "0" |] - let expected = Error (ParseNumberFailure(args[0], (1, Int32.MaxValue))) + let expected = Error (NumberParseFailure(args[0], (1, Int32.MaxValue))) let actual = validate args Assert.Equal(expected, actual) From 183b10a0a04a8b947cb9f1fe89626eba5917d351 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:10:19 +0900 Subject: [PATCH 32/49] Use 'function' for matching --- src/UniqueFileGenerator.Console/Errors.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index 0f80226..b0fea32 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -17,8 +17,7 @@ module Errors = | IoError of string | CancelledByUser - let errorMsg error = - match error with + let errorMsg = function | ArgsMissing -> "You must pass in at least one argument: the number of files to generate." | ArgCountInvalid -> "Invalid arguments. If you submit option flags, each must have a corresponding value." | MalformedFlags -> "Malformed option flag(s) found." From 7f4b5972750ee4c7aeee5c149e3587bdfe3962da Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:11:17 +0900 Subject: [PATCH 33/49] Tweak error message wording --- src/UniqueFileGenerator.Console/Errors.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index b0fea32..4e8d41c 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -28,7 +28,7 @@ module Errors = | DirectoryMissing dirName -> $"Directory \"%s{dirName}\" was not found." | DriveSpaceConfirmationFailure -> "Could not confirm available drive space." | DriveSpaceInsufficient (needed, actual) -> - $"Insufficient drive space. Though %s{needed} is necessary, only %s{actual} is available." + $"Insufficient drive space: %s{needed} is necessary, but only %s{actual} is available." | IoError msg -> $"IO error: %s{msg}" | CancelledByUser -> "Cancelled by the user." From cd48dda80dce3474b4defff67db98d0e4686d7a8 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:27:08 +0900 Subject: [PATCH 34/49] Create stripSeparatorsAndTrim func --- .../Arguments/ArgumentTypes.fs | 17 +++----- .../ArgParserTests.fs | 43 ++++++++++--------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index f8d15e4..26be62e 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -10,7 +10,8 @@ module ArgTypes = let supportedSeparators = [ ","; "_" ] - let stripSeparators = String.stripSubstrings supportedSeparators + let stripSeparatorsAndTrim = + String.stripSubstrings supportedSeparators >> String.trim let private tryParseIntInRange (floor, ceiling) text = text @@ -22,7 +23,7 @@ module ArgTypes = static member Create text : Result = text - |> stripSeparators + |> stripSeparatorsAndTrim |> tryParseInRange FileCount.AllowedRange |> Result.bimap (fun _ -> NumberParseFailure (text, FileCount.AllowedRange)) @@ -47,9 +48,7 @@ module ArgTypes = static member TryCreate text = text |> option - (stripSeparators - >> String.trim - >> tryParseIntInRange NameBaseLength.AllowedRange) + (stripSeparatorsAndTrim >> tryParseIntInRange NameBaseLength.AllowedRange) (Ok NameBaseLength.Default) |> Result.map NameBaseLength @@ -80,9 +79,7 @@ module ArgTypes = static member TryCreate text = text - |> Option.map (stripSeparators - >> String.trim - >> tryParseIntInRange Size.AllowedRange) + |> Option.map (stripSeparatorsAndTrim >> tryParseIntInRange Size.AllowedRange) |> function | Some (Ok i) -> Ok (Size (Some i)) | Some (Error e) -> Error e // Parse error. @@ -97,9 +94,7 @@ module ArgTypes = static member TryCreate text = text |> option - (stripSeparators - >> String.trim - >> tryParseIntInRange Delay.AllowedRange) + (stripSeparatorsAndTrim >> tryParseIntInRange Delay.AllowedRange) (Ok Delay.Default) |> Result.map Delay diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index e2a54c7..8929a8b 100644 --- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs +++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs @@ -250,132 +250,133 @@ module SupportedSeparators = [] let ``Strips commas from text`` () = let text = "hello,world" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("helloworld", result) [] let ``Strips underscores from text`` () = let text = "hello_world" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("helloworld", result) [] - let ``Strips both commas and underscores`` () = - let text = "hello,world_test" - let result = stripSeparators text + let ``Strips both commas and underscores and trims`` () = + let text = " hello,world_test " + let result = stripSeparatorsAndTrim text Assert.Equal("helloworldtest", result) [] let ``Strips multiple consecutive separators`` () = let text = "hello,,__world" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("helloworld", result) [] let ``Returns empty string when input is only separators`` () = let text = ",_,_," - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal(String.Empty, result) [] let ``Returns empty string for empty input`` () = let text = String.Empty - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal(String.Empty, result) [] let ``Returns unchanged text with no separators`` () = let text = "helloworld" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("helloworld", result) [] let ``Preserves whitespace`` () = let text = "hello , world _ test" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("hello world test", result) [] let ``Preserves other punctuation`` () = let text = "hello,world.test_example!done" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("helloworld.testexample!done", result) [] let ``Preserves numbers`` () = let text = "test_123,456_abc" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("test123456abc", result) [] let ``Handles mixed case correctly`` () = let text = "Hello,World_Test" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("HelloWorldTest", result) [] let ``Handles Unicode characters`` () = let text = "café,naïve_résumé,東京と京都" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("cafénaïverésumé東京と京都", result) [] let ``Handles single character input`` () = let text = "_" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal(String.Empty, result) [] let ``Handles single character without separator`` () = let text = "a" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("a", result) [] let ``Handles separators at start`` () = let text = "_,hello" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("hello", result) [] let ``Handles separators at end`` () = let text = "hello_," - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("hello", result) [] let ``Handles separators at both ends`` () = let text = ",_hello_," - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("hello", result) [] let ``Handles large strings`` () = let text = String.replicate 1000 "a_b," - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal(String.replicate 1000 "ab", result) [] let ``Handles tabs and newlines`` () = let text = "hello,world\ttest_example\nmore" - let result = stripSeparators text + let result = stripSeparatorsAndTrim text Assert.Equal("helloworld\ttestexample\nmore", result) + From f02d1ea331c281833647cdb1b81585d4d7864f5b Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:45:08 +0900 Subject: [PATCH 35/49] Rename func --- .../Arguments/ArgumentTypes.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 859972a..3f75792 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -13,7 +13,7 @@ module ArgTypes = let stripSeparatorsAndTrim = String.stripSubstrings supportedSeparators >> String.trim - let private tryParseIntInRange (floor, ceiling) text = + let private parseInRange (floor, ceiling) text = text |> tryParseInRange (floor, ceiling) |> Result.mapError (fun _ -> NumberParseFailure (text, (floor, ceiling))) @@ -48,7 +48,7 @@ module ArgTypes = static member TryCreate maybeText = maybeText |> option - (stripSeparatorsAndTrim >> tryParseIntInRange NameBaseLength.AllowedRange) + (stripSeparatorsAndTrim >> parseInRange NameBaseLength.AllowedRange) (Ok NameBaseLength.Default) |> Result.map NameBaseLength @@ -79,7 +79,7 @@ module ArgTypes = static member TryCreate maybeText = maybeText - |> Option.map (stripSeparatorsAndTrim >> tryParseIntInRange Size.AllowedRange) + |> Option.map (stripSeparatorsAndTrim >> parseInRange Size.AllowedRange) |> function | Some (Ok i) -> Ok (Size (Some i)) | Some (Error e) -> Error e // Parse error. @@ -94,7 +94,7 @@ module ArgTypes = static member TryCreate maybeText = maybeText |> option - (stripSeparatorsAndTrim >> tryParseIntInRange Delay.AllowedRange) + (stripSeparatorsAndTrim >> parseInRange Delay.AllowedRange) (Ok Delay.Default) |> Result.map Delay From 2c1d8db5a0492b8c3a6e75dc563d6f7ed6abbbc1 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 20:19:34 +0900 Subject: [PATCH 36/49] Rename Create to TryCreate --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 2 +- src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs | 2 +- src/UniqueFileGenerator.Tests/ArgParserTests.fs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 3f75792..aee375b 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -21,7 +21,7 @@ module ArgTypes = type FileCount = private FileCount of int with static member val AllowedRange = 1, Int32.MaxValue - static member Create text : Result = + static member TryCreate text : Result = text |> stripSeparatorsAndTrim |> tryParseInRange FileCount.AllowedRange diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index 9b48bf9..7a7cd9b 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -51,7 +51,7 @@ module ArgValidation = result { do! validateArgCount args - let! fileCount = FileCount.Create (Array.head args) + let! fileCount = FileCount.TryCreate (Array.head args) let! optionMap = toPairs (Array.tail args) let tryGetArg x = Map.tryFind flags[x] optionMap diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index 8929a8b..40deba9 100644 --- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs +++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs @@ -36,7 +36,7 @@ let defaultOptions = | Error e -> failwith $"Unexpected parse error: {e}"} let createOkArgs fileCount options = - match FileCount.Create fileCount with + match FileCount.TryCreate fileCount with | Ok fc -> Ok <| Args.Create(fc, options) | Error _ -> failwith "Unexpected error in test setup!" From 29c6a0e71535a9582809257dd7c901a60c397d6a Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:39:44 +0900 Subject: [PATCH 37/49] DU on one line --- .../Arguments/ArgumentTypes.fs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index aee375b..15453eb 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -100,13 +100,7 @@ module ArgTypes = member this.Value = let (Delay length) = this in length - type AppOption = - | Prefix - | NameBaseLength - | Extension - | OutputDirectory - | Size - | Delay + type AppOption = Prefix | NameBaseLength | Extension | OutputDirectory | Size | Delay type Options = { Prefix: string From bf704281fdbb2cf69db7222d0c88c39f18312b21 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:45:39 +0900 Subject: [PATCH 38/49] Minor code tweaks --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index 15453eb..e1441bf 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -25,7 +25,7 @@ module ArgTypes = text |> stripSeparatorsAndTrim |> tryParseInRange FileCount.AllowedRange - |> Result.bimap + |> bimap (fun _ -> NumberParseFailure (text, FileCount.AllowedRange)) FileCount @@ -111,9 +111,7 @@ module ArgTypes = Delay: int } type Args = - private - { fileCount: int - options: Options } + private { fileCount: int; options: Options } member x.FileCount = x.fileCount member x.Options = x.options From 78808e8165d37bcbf2f45848cd3caf672882f6f8 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:47:05 +0900 Subject: [PATCH 39/49] Rename binding --- src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index e1441bf..cae7f9d 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -62,7 +62,7 @@ module ArgTypes = |> option String.trim Extension.Default |> Extension - member this.Value = let (Extension extension) = this in extension + member this.Value = let (Extension ext) = this in ext type OutputDirectory = private OutputDirectory of string with static member val Default = "output" From ad7fd6553268dcfd3c32e7c71381c99bd62f8e60 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:24:32 +0900 Subject: [PATCH 40/49] Refactor generateSingle to use a StringBuilder --- src/UniqueFileGenerator.Console/StringGeneration.fs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index 9a056c2..574cd3f 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -2,6 +2,7 @@ namespace UniqueFileGenerator.Console open System open FSharpPlus +open CCFSharpUtils open CCFSharpUtils.Text module StringGeneration = @@ -15,10 +16,12 @@ module StringGeneration = |> map string |> String.concat String.Empty - let private rnd = Random() + let private rnd = Random.Shared - let private generateSingle length : string = - String(Array.init length (fun _ -> charBank[rnd.Next(charBank.Length)])) + let private generateSingle (length: int) : string = + let sb = SB length + Array.init length (fun _ -> sb.Append charBank[rnd.Next charBank.Length]) |> ignore + sb.ToString() let generateMultiple eachLength count : string array = Array.init count (fun _ -> generateSingle eachLength) From e3f09324656c4ac8889c6896f8dde63e2ea6a030 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:26:33 +0900 Subject: [PATCH 41/49] Update error text --- src/UniqueFileGenerator.Console/Errors.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index 4e8d41c..fcdb43a 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -24,7 +24,7 @@ module Errors = | UnknownFlags -> "Unknown option flag(s) found." | DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once." | NumberParseFailure (input, (floor, ceiling)) -> - $"Cannot parse \"%s{input}\" to an integer between %s{Num.Format floor} and %s{Num.Format ceiling}, inclusive." + $"The number \"%s{input}\" is out of bounds. Enter an integer between %s{Num.Format floor} and %s{Num.Format ceiling}, inclusive." | DirectoryMissing dirName -> $"Directory \"%s{dirName}\" was not found." | DriveSpaceConfirmationFailure -> "Could not confirm available drive space." | DriveSpaceInsufficient (needed, actual) -> From 0e91a7c731d3dc4ff8eef36649cdc93477255ca8 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:37:25 +0900 Subject: [PATCH 42/49] Rename func to tryParseInRange --- .../Arguments/ArgumentTypes.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index cae7f9d..af720fc 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -13,7 +13,7 @@ module ArgTypes = let stripSeparatorsAndTrim = String.stripSubstrings supportedSeparators >> String.trim - let private parseInRange (floor, ceiling) text = + let private tryParseInRange (floor, ceiling) text : Result = text |> tryParseInRange (floor, ceiling) |> Result.mapError (fun _ -> NumberParseFailure (text, (floor, ceiling))) @@ -48,7 +48,7 @@ module ArgTypes = static member TryCreate maybeText = maybeText |> option - (stripSeparatorsAndTrim >> parseInRange NameBaseLength.AllowedRange) + (stripSeparatorsAndTrim >> tryParseInRange NameBaseLength.AllowedRange) (Ok NameBaseLength.Default) |> Result.map NameBaseLength @@ -79,7 +79,7 @@ module ArgTypes = static member TryCreate maybeText = maybeText - |> Option.map (stripSeparatorsAndTrim >> parseInRange Size.AllowedRange) + |> Option.map (stripSeparatorsAndTrim >> tryParseInRange Size.AllowedRange) |> function | Some (Ok i) -> Ok (Size (Some i)) | Some (Error e) -> Error e // Parse error. @@ -94,7 +94,7 @@ module ArgTypes = static member TryCreate maybeText = maybeText |> option - (stripSeparatorsAndTrim >> parseInRange Delay.AllowedRange) + (stripSeparatorsAndTrim >> tryParseInRange Delay.AllowedRange) (Ok Delay.Default) |> Result.map Delay From 3bfe6341bb339736664692e5043be197519cd2ba Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:38:31 +0900 Subject: [PATCH 43/49] Fix string generation test --- src/UniqueFileGenerator.Tests/StringGenerationTests.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs index 8ee01a5..cc235f9 100644 --- a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs +++ b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs @@ -39,7 +39,7 @@ module Strings = [] let ``Throws with negative item length`` () = - Assert.Throws(fun () -> + Assert.Throws(fun () -> (generateMultiple -1 5000) :> obj) module FileNames = From 65dc4a33286ffec0aee0c3a0740ff9aae4a51bdd Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:09:05 +0900 Subject: [PATCH 44/49] Use lists over arrays --- .../StringGeneration.fs | 6 +++--- .../StringGenerationTests.fs | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index 574cd3f..27335df 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -20,11 +20,11 @@ module StringGeneration = let private generateSingle (length: int) : string = let sb = SB length - Array.init length (fun _ -> sb.Append charBank[rnd.Next charBank.Length]) |> ignore + List.init length (fun _ -> sb.Append charBank[rnd.Next charBank.Length]) |> ignore sb.ToString() - let generateMultiple eachLength count : string array = - Array.init count (fun _ -> generateSingle eachLength) + let generateMultiple eachLength count : string list = + List.init count (fun _ -> generateSingle eachLength) // TODO: Consider returning an actual file object. let toFileName parts : string = diff --git a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs index cc235f9..5f99077 100644 --- a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs +++ b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs @@ -12,7 +12,7 @@ module Strings = let count = 222 let generated = generateMultiple itemLength count - Assert.True(generated |> Array.forall (fun x -> x.Length = itemLength)) + Assert.True(generated |> List.forall (fun x -> x.Length = itemLength)) Assert.True(generated.Length = count) [] @@ -29,7 +29,7 @@ module Strings = let count = 222 let generated = generateMultiple itemLength count - Assert.True(generated |> Array.forall (fun x -> x.Length = itemLength)) + Assert.True(generated |> List.forall (fun x -> x.Length = itemLength)) Assert.True(generated.Length = count) [] @@ -53,7 +53,7 @@ module FileNames = generated |> map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) - Assert.Equal(generated, fileNames) + Assert.Equal(generated, fileNames) [] let ``Generates filenames with a prefix and no extension`` () = @@ -65,7 +65,7 @@ module FileNames = generated |> map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) - Assert.True(fileNames |> Array.forall (fun x -> x.StartsWith prefix)) + Assert.True(fileNames |> List.forall (fun x -> x.StartsWith prefix)) [] let ``Generates filenames with an extension and no prefix`` () = @@ -77,7 +77,7 @@ module FileNames = generated |> map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) - Assert.True(fileNames |> Array.forall (fun x -> x.EndsWith extension)) + Assert.True(fileNames |> List.forall (fun x -> x.EndsWith extension)) [] let ``Generates filenames with a prefix and extension args`` () = @@ -89,5 +89,5 @@ module FileNames = generated |> map (fun x -> toFileName { Prefix = prefix; Base = x; Ext = extension }) - Assert.True(fileNames |> Array.forall (fun x -> x.StartsWith prefix)) - Assert.True(fileNames |> Array.forall (fun x -> x.EndsWith extension)) + Assert.True(fileNames |> List.forall (fun x -> x.StartsWith prefix)) + Assert.True(fileNames |> List.forall (fun x -> x.EndsWith extension)) From 0229e200ddea64eeab9813e417b1dad9a37a28b2 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:28:19 +0900 Subject: [PATCH 45/49] Use fixed character bank --- src/UniqueFileGenerator.Console/StringGeneration.fs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index 27335df..75b80cf 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -7,14 +7,9 @@ open CCFSharpUtils.Text module StringGeneration = type FileNameParts = - { Prefix: string - Base: string - Ext: string } + { Prefix: string; Base: string; Ext: string } - let private charBank = - [ 'A' .. 'Z' ] @ [ 'a' .. 'z' ] @ [ '0' .. '9' ] - |> map string - |> String.concat String.Empty + let private charBank = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" let private rnd = Random.Shared @@ -32,7 +27,7 @@ module StringGeneration = match parts.Ext.Trim() with | ext when String.hasNoText ext -> String.Empty | ext when ext.StartsWith '.' -> ext - | ext -> $".%s{ext}" + | ext -> $".{ext}" String.Concat( parts.Prefix.Trim(), From ab634af4ff77b30e7eb1c795d7005830e8bcf037 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:30:12 +0900 Subject: [PATCH 46/49] Rename func --- src/UniqueFileGenerator.Console/StringGeneration.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index 75b80cf..9395f62 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -6,8 +6,8 @@ open CCFSharpUtils open CCFSharpUtils.Text module StringGeneration = - type FileNameParts = - { Prefix: string; Base: string; Ext: string } + + type FileNameParts = { Prefix: string; Base: string; Ext: string } let private charBank = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" @@ -23,7 +23,7 @@ module StringGeneration = // TODO: Consider returning an actual file object. let toFileName parts : string = - let sanitizedExtension = + let sanitizedExt = match parts.Ext.Trim() with | ext when String.hasNoText ext -> String.Empty | ext when ext.StartsWith '.' -> ext @@ -32,7 +32,7 @@ module StringGeneration = String.Concat( parts.Prefix.Trim(), parts.Base, - sanitizedExtension) + sanitizedExt) let generateFileContent sizeInBytes fallback : string = sizeInBytes |> option generateSingle fallback From bb761efc37a254e3f4a63cd0cab2d272e21fe8d5 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:33:49 +0900 Subject: [PATCH 47/49] Don't trim the prefix --- src/UniqueFileGenerator.Console/StringGeneration.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index 9395f62..57db2a0 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -27,10 +27,10 @@ module StringGeneration = match parts.Ext.Trim() with | ext when String.hasNoText ext -> String.Empty | ext when ext.StartsWith '.' -> ext - | ext -> $".{ext}" + | ext -> $".%s{ext}" String.Concat( - parts.Prefix.Trim(), + parts.Prefix, parts.Base, sanitizedExt) From 593aaf6fa33eecad39c34e30e4713cd84cae606c Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sat, 19 Sep 2026 20:41:55 +0900 Subject: [PATCH 48/49] Update test --- src/UniqueFileGenerator.Tests/StringGenerationTests.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs index 5f99077..adb3a42 100644 --- a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs +++ b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs @@ -56,8 +56,8 @@ module FileNames = Assert.Equal(generated, fileNames) [] - let ``Generates filenames with a prefix and no extension`` () = - let prefix = "@@" + let ``Generates filenames with a prefix (respecting spaces) and no extension`` () = + let prefix = " @@ " let extension = String.Empty let generated = generateMultiple 10 10 From 0c735e88c73ddb89dac8e0487725142a1a764b06 Mon Sep 17 00:00:00 2001 From: CodeConscious <50596087+codeconscious@users.noreply.github.com> Date: Sun, 20 Sep 2026 11:43:55 +0900 Subject: [PATCH 49/49] Re-update CCFSharpUtils --- .../UniqueFileGenerator.Console.fsproj | 2 +- src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj index 402401e..939946a 100644 --- a/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj +++ b/src/UniqueFileGenerator.Console/UniqueFileGenerator.Console.fsproj @@ -20,7 +20,7 @@ - + diff --git a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj index 4166da2..886a5a3 100644 --- a/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj +++ b/src/UniqueFileGenerator.Tests/UniqueFileGenerator.Tests.fsproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive