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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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 9d78de20c21e2d3e0f386e718aadd0c2c53deead Mon Sep 17 00:00:00 2001
From: CodeConscious <50596087+codeconscious@users.noreply.github.com>
Date: Sat, 19 Sep 2026 20:42:45 +0900
Subject: [PATCH 48/55] Check for invalid chars
---
.../Arguments/ArgumentTypes.fs | 15 ++++++++++++---
.../Arguments/ArgumentValidation.fs | 2 +-
src/UniqueFileGenerator.Console/Errors.fs | 6 ++++++
3 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
index af720fc..7002b56 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
@@ -3,6 +3,7 @@ namespace UniqueFileGenerator.Console
open Errors
open Utilities
open System
+open System.IO
open FSharpPlus
open CCFSharpUtils.Text
@@ -18,6 +19,14 @@ module ArgTypes =
|> tryParseInRange (floor, ceiling)
|> Result.mapError (fun _ -> NumberParseFailure (text, (floor, ceiling)))
+ /// Determines whether a string contains characters invalid for filnames on this OS.
+ let validateChars text =
+ let isInvalid ch = Array.contains ch ([|'!'; '.'|])
+ let invalidChars = text |> filter isInvalid |> String.toList
+ match invalidChars with
+ | [] -> Ok text
+ | _ -> Error (InvalidChars invalidChars)
+
type FileCount = private FileCount of int with
static member val AllowedRange = 1, Int32.MaxValue
@@ -35,9 +44,9 @@ module ArgTypes =
static member val Default = String.Empty
static member Create maybeText =
- maybeText
- |> option id Prefix.Default
- |> Prefix
+ match maybeText with
+ | None -> Ok (Prefix Prefix.Default)
+ | Some text -> text |> validateChars |> Result.map Prefix
member this.Value = let (Prefix prefix) = this in prefix
diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs
index 7a7cd9b..852e746 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs
@@ -55,7 +55,7 @@ module ArgValidation =
let! optionMap = toPairs (Array.tail args)
let tryGetArg x = Map.tryFind flags[x] optionMap
- 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)
diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs
index fcdb43a..a0a6557 100644
--- a/src/UniqueFileGenerator.Console/Errors.fs
+++ b/src/UniqueFileGenerator.Console/Errors.fs
@@ -1,6 +1,7 @@
namespace UniqueFileGenerator.Console
open Utilities
+open CCFSharpUtils.Text
module Errors =
@@ -11,6 +12,7 @@ module Errors =
| UnknownFlags
| DuplicateFlags
| NumberParseFailure of Input: string * AllowedRange: (int * int)
+ | InvalidChars of Chars: char list
| DirectoryMissing of string
| DriveSpaceConfirmationFailure
| DriveSpaceInsufficient of Needed: string * Actual: string
@@ -25,6 +27,10 @@ module Errors =
| DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once."
| NumberParseFailure (input, (floor, ceiling)) ->
$"The number \"%s{input}\" is out of bounds. Enter an integer between %s{Num.Format floor} and %s{Num.Format ceiling}, inclusive."
+ | InvalidChars chars ->
+ sprintf "Found %s invalid for file names on this system: \"%s\"."
+ (String.pluralizeSWithCount "character" chars.Length)
+ (chars |> List.map string |> String.concat "\", \"")
| DirectoryMissing dirName -> $"Directory \"%s{dirName}\" was not found."
| DriveSpaceConfirmationFailure -> "Could not confirm available drive space."
| DriveSpaceInsufficient (needed, actual) ->
From 7434f1a27e81dcb7e1b3efe1e1bef8e9894606d6 Mon Sep 17 00:00:00 2001
From: CodeConscious <50596087+codeconscious@users.noreply.github.com>
Date: Sat, 19 Sep 2026 21:34:00 +0900
Subject: [PATCH 49/55] Use actual invalid-paths method
---
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 7002b56..0950efc 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
@@ -21,7 +21,7 @@ module ArgTypes =
/// Determines whether a string contains characters invalid for filnames on this OS.
let validateChars text =
- let isInvalid ch = Array.contains ch ([|'!'; '.'|])
+ let isInvalid ch = Array.contains ch (Path.GetInvalidPathChars())
let invalidChars = text |> filter isInvalid |> String.toList
match invalidChars with
| [] -> Ok text
From 8e8b5f18570624d989682dcdf39192ac29f8b92b Mon Sep 17 00:00:00 2001
From: CodeConscious <50596087+codeconscious@users.noreply.github.com>
Date: Sat, 19 Sep 2026 21:54:23 +0900
Subject: [PATCH 50/55] Validate extension chars too
---
.../Arguments/ArgumentTypes.fs | 12 +++++++-----
.../Arguments/ArgumentValidation.fs | 2 +-
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
index 0950efc..9515b7e 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
@@ -45,8 +45,9 @@ module ArgTypes =
static member Create maybeText =
match maybeText with
- | None -> Ok (Prefix Prefix.Default)
- | Some text -> text |> validateChars |> Result.map Prefix
+ | Some text -> validateChars text
+ | None -> Ok Prefix.Default
+ |> map Prefix
member this.Value = let (Prefix prefix) = this in prefix
@@ -67,9 +68,10 @@ module ArgTypes =
static member val Default = String.Empty
static member Create maybeText =
- maybeText
- |> option String.trim Extension.Default
- |> Extension
+ match maybeText with
+ | Some text -> text |> validateChars |> map String.trim
+ | None -> Ok Extension.Default
+ |> map Extension
member this.Value = let (Extension ext) = this in ext
diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs
index 852e746..64fa284 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs
@@ -57,7 +57,7 @@ module ArgValidation =
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)
+ let! e = Extension.Create (tryGetArg Extension)
let o = OutputDirectory.Create (tryGetArg OutputDirectory)
let! s = Size.TryCreate (tryGetArg Size)
let! d = Delay.TryCreate (tryGetArg Delay)
From 9931c15be4ca5be8a67738e8e647b4eab61c7ca9 Mon Sep 17 00:00:00 2001
From: CodeConscious <50596087+codeconscious@users.noreply.github.com>
Date: Sat, 19 Sep 2026 21:59:20 +0900
Subject: [PATCH 51/55] Shorten option blocks
---
.../Arguments/ArgumentTypes.fs | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
index 9515b7e..6b883d0 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
@@ -44,9 +44,8 @@ module ArgTypes =
static member val Default = String.Empty
static member Create maybeText =
- match maybeText with
- | Some text -> validateChars text
- | None -> Ok Prefix.Default
+ maybeText
+ |> option validateChars (Ok Prefix.Default)
|> map Prefix
member this.Value = let (Prefix prefix) = this in prefix
@@ -68,9 +67,8 @@ module ArgTypes =
static member val Default = String.Empty
static member Create maybeText =
- match maybeText with
- | Some text -> text |> validateChars |> map String.trim
- | None -> Ok Extension.Default
+ maybeText
+ |> option (validateChars >> map String.trim) (Ok Extension.Default)
|> map Extension
member this.Value = let (Extension ext) = this in ext
From cb9764b482a3f037c3414956ced45c84318b9082 Mon Sep 17 00:00:00 2001
From: CodeConscious <50596087+codeconscious@users.noreply.github.com>
Date: Sat, 19 Sep 2026 22:17:00 +0900
Subject: [PATCH 52/55] Update test
---
.../Arguments/ArgumentTypes.fs | 2 +-
src/UniqueFileGenerator.Tests/ArgParserTests.fs | 11 ++++++++---
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
index 6b883d0..5b4ecbc 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
@@ -59,7 +59,7 @@ module ArgTypes =
|> option
(stripSeparatorsAndTrim >> tryParseInRange NameBaseLength.AllowedRange)
(Ok NameBaseLength.Default)
- |> Result.map NameBaseLength
+ |> map NameBaseLength
member this.Value = let (NameBaseLength length) = this in length
diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs
index 40deba9..2e02bfd 100644
--- a/src/UniqueFileGenerator.Tests/ArgParserTests.fs
+++ b/src/UniqueFileGenerator.Tests/ArgParserTests.fs
@@ -18,13 +18,19 @@ let validOptionValues =
Delay, "5_000" ]
let defaultOptions =
- { Prefix = Prefix.Create None |> _.Value
+ { Prefix = Prefix.Create None
+ |> function
+ | Ok x -> x.Value
+ | Error e -> failwith $"Unexpected parse error: {e}"
NameBaseLength =
NameBaseLength.TryCreate None
|> function
| Ok x -> x.Value
| Error e -> failwith $"Unexpected parse error: {e}"
- Extension = Extension.Create None |> _.Value
+ Extension = Extension.Create None
+ |> function
+ | Ok x -> x.Value
+ | Error e -> failwith $"Unexpected parse error: {e}"
OutputDirectory = OutputDirectory.Create None |> _.Value
Size = Size.TryCreate None
|> function
@@ -379,4 +385,3 @@ module SupportedSeparators =
let result = stripSeparatorsAndTrim text
Assert.Equal("helloworld\ttestexample\nmore", result)
-
From 26cddf555d7b318a69d5f73b949e4c0d88ae143c Mon Sep 17 00:00:00 2001
From: CodeConscious <50596087+codeconscious@users.noreply.github.com>
Date: Sun, 20 Sep 2026 10:36:31 +0900
Subject: [PATCH 53/55] Tweak error code
---
src/UniqueFileGenerator.Console/Errors.fs | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs
index a0a6557..01ca52a 100644
--- a/src/UniqueFileGenerator.Console/Errors.fs
+++ b/src/UniqueFileGenerator.Console/Errors.fs
@@ -2,6 +2,7 @@ namespace UniqueFileGenerator.Console
open Utilities
open CCFSharpUtils.Text
+open FSharpPlus
module Errors =
@@ -26,11 +27,12 @@ module Errors =
| UnknownFlags -> "Unknown option flag(s) found."
| DuplicateFlags -> "Duplicate option flag(s) found. Each can only be used once."
| NumberParseFailure (input, (floor, ceiling)) ->
- $"The number \"%s{input}\" is out of bounds. Enter an integer between %s{Num.Format floor} and %s{Num.Format ceiling}, inclusive."
+ sprintf "\"%s\" is an invalid value. Enter an integer between %s and %s, inclusive."
+ input (Num.Format floor) (Num.Format ceiling)
| InvalidChars chars ->
sprintf "Found %s invalid for file names on this system: \"%s\"."
- (String.pluralizeSWithCount "character" chars.Length)
- (chars |> List.map string |> String.concat "\", \"")
+ (String.pluralizeSWithCount "character" chars.Length) // "1 character," "2 characters," etc.
+ (chars |> map string |> String.concat "\", \"")
| DirectoryMissing dirName -> $"Directory \"%s{dirName}\" was not found."
| DriveSpaceConfirmationFailure -> "Could not confirm available drive space."
| DriveSpaceInsufficient (needed, actual) ->
From 51bb48d4cbf3462e8a3b2b9037383e5685769243 Mon Sep 17 00:00:00 2001
From: CodeConscious <50596087+codeconscious@users.noreply.github.com>
Date: Sun, 20 Sep 2026 11:19:28 +0900
Subject: [PATCH 54/55] Refactor validateChars
---
.../Arguments/ArgumentTypes.fs | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
index 5b4ecbc..5d20173 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
@@ -20,12 +20,13 @@ module ArgTypes =
|> Result.mapError (fun _ -> NumberParseFailure (text, (floor, ceiling)))
/// Determines whether a string contains characters invalid for filnames on this OS.
- let validateChars text =
- let isInvalid ch = Array.contains ch (Path.GetInvalidPathChars())
- let invalidChars = text |> filter isInvalid |> String.toList
- match invalidChars with
- | [] -> Ok text
- | _ -> Error (InvalidChars invalidChars)
+ let validateChars (text: string) =
+ let invalidChars = Path.GetInvalidFileNameChars() |> Set.ofArray
+ let isInvalidChar ch = Set.contains ch invalidChars
+ let rejectedChars = filter isInvalidChar text
+ if String.hasNoText rejectedChars
+ then Ok text
+ else Error (rejectedChars |> String.toList |> InvalidChars)
type FileCount = private FileCount of int with
static member val AllowedRange = 1, Int32.MaxValue
From 303ebb7c180a3d661fa09757af58ae2ddea096c7 Mon Sep 17 00:00:00 2001
From: CodeConscious <50596087+codeconscious@users.noreply.github.com>
Date: Sun, 20 Sep 2026 11:28:48 +0900
Subject: [PATCH 55/55] Revert 'String.'
---
src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
index 5d20173..4ee08ef 100644
--- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
+++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
@@ -20,10 +20,10 @@ module ArgTypes =
|> Result.mapError (fun _ -> NumberParseFailure (text, (floor, ceiling)))
/// Determines whether a string contains characters invalid for filnames on this OS.
- let validateChars (text: string) =
+ let validateChars text =
let invalidChars = Path.GetInvalidFileNameChars() |> Set.ofArray
let isInvalidChar ch = Set.contains ch invalidChars
- let rejectedChars = filter isInvalidChar text
+ let rejectedChars = String.filter isInvalidChar text
if String.hasNoText rejectedChars
then Ok text
else Error (rejectedChars |> String.toList |> InvalidChars)