Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2d5df29
Update to .NET 10
codeconscious Sep 14, 2026
5247869
Update packages
codeconscious Sep 14, 2026
e198481
Add CCFSharpUtils
codeconscious Sep 14, 2026
ee8ac5d
Add separator tests
codeconscious Sep 14, 2026
8a6ebab
Use String.stripSubstrings from my library
codeconscious Sep 14, 2026
2f843af
Combine Option.map calls
codeconscious Sep 14, 2026
039e5ba
Use the F#+ option keyword
codeconscious Sep 14, 2026
00bcd7a
Use String.hasNoText
codeconscious Sep 15, 2026
7e70cbe
Use option keyword
codeconscious Sep 15, 2026
8d24c2c
Update package
codeconscious Sep 15, 2026
58d4109
Use Num.isEven
codeconscious Sep 15, 2026
4d480ef
Various updates
codeconscious Sep 15, 2026
23b1697
Remove superfluous sleep function
codeconscious Sep 15, 2026
97c6028
Use map and iter; etc.
codeconscious Sep 15, 2026
f8a0b43
Rename OptionType to AppOption
codeconscious Sep 16, 2026
9ce259b
Rename ErrorType to AppError
codeconscious Sep 16, 2026
3bb87b1
Rename getMessage to errorMsg and make minor tweaks
codeconscious Sep 16, 2026
375a7b8
Improve matching
codeconscious Sep 16, 2026
1985de8
Nomenclature
codeconscious Sep 16, 2026
b470de5
Use Array.head and .tail; etc.
codeconscious Sep 16, 2026
7abbce2
Remove unneeded variables
codeconscious Sep 16, 2026
5033dac
Refactor ArgumentValidation
codeconscious Sep 17, 2026
da0588f
Use Result.bimap
codeconscious Sep 17, 2026
376fdd5
Rename function
codeconscious Sep 17, 2026
ed187dc
Rename function to tryParseInRange
codeconscious Sep 17, 2026
31211bb
Replace 3 functions with overloaded methods
codeconscious Sep 17, 2026
916a30f
Move Num type culture to static let
codeconscious Sep 17, 2026
35c0cd0
Code layout
codeconscious Sep 17, 2026
8cab1a8
Rename text to maybeText
codeconscious Sep 17, 2026
72efee5
Remove parens
codeconscious Sep 17, 2026
991394f
Rename errors; tweak error messages
codeconscious Sep 17, 2026
183b10a
Use 'function' for matching
codeconscious Sep 17, 2026
7f4b597
Tweak error message wording
codeconscious Sep 17, 2026
cd48dda
Create stripSeparatorsAndTrim func
codeconscious Sep 17, 2026
a0823dc
Merge remote-tracking branch 'refs/remotes/origin/2026-tweaks' into 2…
codeconscious Sep 17, 2026
f02d1ea
Rename func
codeconscious Sep 17, 2026
2c1d8db
Rename Create to TryCreate
codeconscious Sep 18, 2026
29c6a0e
DU on one line
codeconscious Sep 18, 2026
bf70428
Minor code tweaks
codeconscious Sep 18, 2026
78808e8
Rename binding
codeconscious Sep 18, 2026
ad7fd65
Refactor generateSingle to use a StringBuilder
codeconscious Sep 19, 2026
e3f0932
Update error text
codeconscious Sep 19, 2026
0e91a7c
Rename func to tryParseInRange
codeconscious Sep 19, 2026
3bfe634
Fix string generation test
codeconscious Sep 19, 2026
65dc4a3
Use lists over arrays
codeconscious Sep 19, 2026
0229e20
Use fixed character bank
codeconscious Sep 19, 2026
ab634af
Rename func
codeconscious Sep 19, 2026
bb761ef
Don't trim the prefix
codeconscious Sep 19, 2026
593aaf6
Update test
codeconscious Sep 19, 2026
e8cd3a9
Merge branch 'main' into 2026-tweaks
codeconscious Sep 20, 2026
0c735e8
Re-update CCFSharpUtils
codeconscious Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 44 additions & 54 deletions src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,40 @@ namespace UniqueFileGenerator.Console
open Errors
open Utilities
open System
open FsToolkit.ErrorHandling
open FSharpPlus
open CCFSharpUtils.Text

module ArgTypes =
let stripSeparators text : string =
let supportedSeparators = [ ","; "_" ]

(text, supportedSeparators)
||> List.fold (fun acc s -> acc.Replace(s, String.Empty))
let supportedSeparators = [ ","; "_" ]

let private tryParseIntInRange (floor, ceiling) text =
let stripSeparatorsAndTrim =
String.stripSubstrings supportedSeparators >> String.trim

let private tryParseInRange (floor, ceiling) text : Result<int, AppError> =
text
|> parseInRange (floor, ceiling)
|> Result.mapError (fun _ -> ParseNumberFailure (text, (floor, ceiling)))
|> tryParseInRange (floor, ceiling)
|> Result.mapError (fun _ -> NumberParseFailure (text, (floor, ceiling)))

type FileCount = private FileCount of int with
static member val AllowedRange = 1, Int32.MaxValue

static member Create (text: string) =
static member TryCreate text : Result<FileCount, AppError> =
text
|> stripSeparators
|> parseInRange FileCount.AllowedRange
|> Result.map FileCount
|> Result.mapError (fun _ -> ParseNumberFailure (text, FileCount.AllowedRange))
|> stripSeparatorsAndTrim
|> tryParseInRange FileCount.AllowedRange
|> bimap
(fun _ -> NumberParseFailure (text, FileCount.AllowedRange))
FileCount

member this.Value = let (FileCount count) = this in count

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 maybeText =
maybeText
|> option id Prefix.Default
|> Prefix

member this.Value = let (Prefix prefix) = this in prefix
Expand All @@ -44,71 +45,62 @@ module ArgTypes =
static member val AllowedRange = 1, 100
static member val Default = 50

static member TryCreate (text: string option) =
text
|> Option.map stripSeparators
|> Option.map (fun arg -> arg.Trim() |> tryParseIntInRange NameBaseLength.AllowedRange)
|> Option.defaultValue (Ok NameBaseLength.Default)
static member TryCreate maybeText =
maybeText
|> option
(stripSeparatorsAndTrim >> tryParseInRange NameBaseLength.AllowedRange)
(Ok NameBaseLength.Default)
|> Result.map NameBaseLength

member this.Value = let (NameBaseLength length) = this in length

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 maybeText =
maybeText
|> 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"

static member Create (text: string option) =
match text with
| None -> OutputDirectory.Default
| Some x -> x.Trim()
static member Create maybeText =
maybeText
|> option String.trim OutputDirectory.Default
|> OutputDirectory

member this.Value = let (OutputDirectory dir) = this in dir

type Size = private Size of int option with
static member val AllowedRange = 1, Int32.MaxValue

static member TryCreate (text: string option) =
text
|> Option.map stripSeparators
|> Option.map (fun arg -> arg.Trim() |> tryParseIntInRange Size.AllowedRange)
static member TryCreate maybeText =
maybeText
|> Option.map (stripSeparatorsAndTrim >> tryParseInRange 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

type Delay = private Delay of int with
static member val AllowedRange = 0, Int32.MaxValue
static member val Default = 0

static member TryCreate (text: string option) =
text
|> Option.map stripSeparators
|> Option.map (fun arg -> arg.Trim() |> tryParseIntInRange Delay.AllowedRange)
|> Option.defaultValue (Ok Delay.Default)
static member TryCreate maybeText =
maybeText
|> option
(stripSeparatorsAndTrim >> tryParseInRange Delay.AllowedRange)
(Ok Delay.Default)
|> Result.map Delay

member this.Value = let (Delay length) = this in length

type OptionType =
| Prefix
| NameBaseLength
| Extension
| OutputDirectory
| Size
| Delay
type AppOption = Prefix | NameBaseLength | Extension | OutputDirectory | Size | Delay

type Options =
{ Prefix: string
Expand All @@ -119,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
Expand All @@ -136,7 +126,7 @@ module ArgTypes =
Size = options.Size
Delay = options.Delay } }

let flags: Map<OptionType, string> =
let flags: Map<AppOption, string> =
[ Prefix, "-p"
NameBaseLength, "-b"
Extension, "-e"
Expand All @@ -146,4 +136,4 @@ module ArgTypes =
|> Map.ofList

let fileNameLength options =
(options.Prefix.Length + options.NameBaseLength + options.Extension.Length)
options.Prefix.Length + options.NameBaseLength + options.Extension.Length
93 changes: 41 additions & 52 deletions src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs
Original file line number Diff line number Diff line change
@@ -1,75 +1,64 @@
namespace UniqueFileGenerator.Console

open UniqueFileGenerator.Console
open System
open ArgTypes
open Errors
open System
open CCFSharpUtils
open FsToolkit.ErrorHandling
open ArgTypes

module ArgValidation =
let private verifyArgCount (args: string array) =
let isEven i = i % 2 = 0

/// 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 isEven l -> Error ArgCountInvalid
| 0 -> Error ArgsMissing
| l when Num.isEven l -> Error ArgCountInvalid
| _ -> Ok ()

let private toPairs (argPairs: string array) =
let private validateOptionArgs (optionMap: Map<string, string>) =
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
let uniqueLength = xs |> Set.ofSeq |> Set.count
originalLength <> uniqueLength

argPairs
|> Array.chunkBySize 2 // Will throw if array length is odd!
|> Array.map (fun x -> x[0].ToLowerInvariant(), x[1])
args
|> Array.chunkBySize 2
|> Array.map (fun pair -> pair[0].ToLowerInvariant(), pair[1])
|> fun pairs ->
match pairs |> Array.map fst |> hasDuplicate with
| true -> Error DuplicateFlags
| false -> Ok (Map.ofArray pairs)

let private verifyOptionArgs (optionPairs: Map<string, string>) =
let hasMalformedOption optionPairs =
let isCorrectFormat (o: string) =
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

match optionPairs with
| o when o.Keys |> hasMalformedOption -> Error MalformedFlags
| o when o.Keys |> hasUnsupportedOption -> Error UnsupportedFlags
| _ -> Ok ()
// 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 validateOptionArgs (Map.ofArray pairs)

let validate args =
result {
do! verifyArgCount args
let fileCountArg, optionArgs = args[0], args[1..]

let! count = FileCount.Create fileCountArg
do! validateArgCount args

let! optionArgPairs = optionArgs |> toPairs
do! verifyOptionArgs optionArgPairs
let tryGetArg x = optionArgPairs |> Map.tryFind flags[x]
let! fileCount = FileCount.TryCreate (Array.head args)
let! optionMap = toPairs (Array.tail args)

let p = Prefix.Create (tryGetArg Prefix)
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 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)

Expand All @@ -81,5 +70,5 @@ module ArgValidation =
Size = s.Value
Delay = d.Value }

return Args.Create(count, options)
return Args.Create(fileCount, options)
}
32 changes: 16 additions & 16 deletions src/UniqueFileGenerator.Console/Errors.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,32 @@ namespace UniqueFileGenerator.Console
open Utilities

module Errors =
type ErrorType =
| NoArgsPassed

type AppError =
| ArgsMissing
| ArgCountInvalid
| MalformedFlags
| UnsupportedFlags
| 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
| IoError of string
| CancelledByUser

let getMessage 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."
| UnsupportedFlags -> "Unsupported flag(s) found."
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."
| UnknownFlags -> "Unknown option 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."
| 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."
| 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}"
| CancelledByUser -> "Cancelled."
$"Insufficient drive space: %s{needed} is necessary, but only %s{actual} is available."
| IoError msg -> $"IO error: %s{msg}"
| CancelledByUser -> "Cancelled by the user."

2 changes: 1 addition & 1 deletion src/UniqueFileGenerator.Console/Help.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading
Loading