diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs index eba7210..af720fc 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentTypes.fs @@ -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 = 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 = 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 @@ -44,11 +45,11 @@ 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 @@ -56,21 +57,19 @@ 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 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 @@ -78,14 +77,13 @@ module ArgTypes = 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 @@ -93,22 +91,16 @@ module ArgTypes = 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 @@ -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 @@ -136,7 +126,7 @@ module ArgTypes = Size = options.Size Delay = options.Delay } } - let flags: Map = + let flags: Map = [ Prefix, "-p" NameBaseLength, "-b" Extension, "-e" @@ -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 diff --git a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs index 62daa4c..7a7cd9b 100644 --- a/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs +++ b/src/UniqueFileGenerator.Console/Arguments/ArgumentValidation.fs @@ -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) = + 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) = - 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) @@ -81,5 +70,5 @@ module ArgValidation = Size = s.Value Delay = d.Value } - return Args.Create(count, options) + return Args.Create(fileCount, options) } diff --git a/src/UniqueFileGenerator.Console/Errors.fs b/src/UniqueFileGenerator.Console/Errors.fs index 3eb216a..fcdb43a 100644 --- a/src/UniqueFileGenerator.Console/Errors.fs +++ b/src/UniqueFileGenerator.Console/Errors.fs @@ -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." 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/Io.fs b/src/UniqueFileGenerator.Console/Io.fs index b5f71e9..6b4c702 100644 --- a/src/UniqueFileGenerator.Console/Io.fs +++ b/src/UniqueFileGenerator.Console/Io.fs @@ -8,25 +8,28 @@ open Utilities open System open System.IO open System.Threading +open FSharpPlus.Operators +open CCFSharpUtils.Operators +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) - | _ -> 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 @@ -41,17 +44,17 @@ module Io = let confirmContinueDespiteLargeSize availableSpace : bool = let ratio = float neededSpace / float availableSpace let isLargeRatio = ratio > warningRatio + let yesAnswers = [| "y"; "yes" |] let confirm () = 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() - [| "y"; "yes" |] - |> Array.exists (fun yesAnswer -> reply.Equals(yesAnswer, StringComparison.InvariantCultureIgnoreCase)) + Array.exists (String.equalIgnoreCase reply) yesAnswers if isLargeRatio then confirm () @@ -75,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) @@ -83,7 +86,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, @@ -96,17 +99,13 @@ 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 + |> 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/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.Console/Program.fs b/src/UniqueFileGenerator.Console/Program.fs index d132bc2..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 @@ -35,7 +36,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 diff --git a/src/UniqueFileGenerator.Console/StringGeneration.fs b/src/UniqueFileGenerator.Console/StringGeneration.fs index 433cc3b..57db2a0 100644 --- a/src/UniqueFileGenerator.Console/StringGeneration.fs +++ b/src/UniqueFileGenerator.Console/StringGeneration.fs @@ -1,39 +1,38 @@ namespace UniqueFileGenerator.Console open System +open FSharpPlus +open CCFSharpUtils +open CCFSharpUtils.Text module StringGeneration = - type FileNameParts = - { Prefix: string - Base: string - Ext: string } - let private charBank = - [ 'A' .. 'Z' ] @ [ 'a' .. 'z' ] @ [ '0' .. '9' ] - |> List.map string - |> String.concat String.Empty + type FileNameParts = { Prefix: string; Base: string; Ext: string } - let private rnd = Random() + let private charBank = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - let private generateSingle length : string = - String(Array.init length (fun _ -> charBank[rnd.Next(charBank.Length)])) + let private rnd = Random.Shared - let generateMultiple eachLength count : string array = - Array.init count (fun _ -> generateSingle eachLength) + let private generateSingle (length: int) : string = + let sb = SB length + List.init length (fun _ -> sb.Append charBank[rnd.Next charBank.Length]) |> ignore + sb.ToString() + let generateMultiple eachLength count : string list = + List.init count (fun _ -> generateSingle eachLength) + + // TODO: Consider returning an actual file object. let toFileName parts : string = - let sanitizedExtension = + let sanitizedExt = 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}" String.Concat( - parts.Prefix.Trim(), + parts.Prefix, parts.Base, - sanitizedExtension) + sanitizedExt) let generateFileContent sizeInBytes fallback : string = - sizeInBytes - |> Option.map generateSingle - |> Option.defaultValue fallback + sizeInBytes |> option generateSingle fallback 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.Console/Utilities.fs b/src/UniqueFileGenerator.Console/Utilities.fs index a9f4cbe..39c5fd3 100644 --- a/src/UniqueFileGenerator.Console/Utilities.fs +++ b/src/UniqueFileGenerator.Console/Utilities.fs @@ -4,27 +4,23 @@ 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 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 () + + // 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) diff --git a/src/UniqueFileGenerator.Tests/ArgParserTests.fs b/src/UniqueFileGenerator.Tests/ArgParserTests.fs index efd8da7..40deba9 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" @@ -36,14 +36,14 @@ 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!" [] 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) @@ -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) @@ -244,3 +244,139 @@ 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 = stripSeparatorsAndTrim text + + Assert.Equal("helloworld", result) + + [] + let ``Strips underscores from text`` () = + let text = "hello_world" + let result = stripSeparatorsAndTrim text + + Assert.Equal("helloworld", result) + + [] + 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 = stripSeparatorsAndTrim text + + Assert.Equal("helloworld", result) + + [] + let ``Returns empty string when input is only separators`` () = + let text = ",_,_," + let result = stripSeparatorsAndTrim text + + Assert.Equal(String.Empty, result) + + [] + let ``Returns empty string for empty input`` () = + let text = String.Empty + let result = stripSeparatorsAndTrim text + + Assert.Equal(String.Empty, result) + + [] + let ``Returns unchanged text with no separators`` () = + let text = "helloworld" + let result = stripSeparatorsAndTrim text + + Assert.Equal("helloworld", result) + + [] + let ``Preserves whitespace`` () = + let text = "hello , world _ test" + let result = stripSeparatorsAndTrim text + + Assert.Equal("hello world test", result) + + [] + let ``Preserves other punctuation`` () = + let text = "hello,world.test_example!done" + let result = stripSeparatorsAndTrim text + + Assert.Equal("helloworld.testexample!done", result) + + [] + let ``Preserves numbers`` () = + let text = "test_123,456_abc" + let result = stripSeparatorsAndTrim text + + Assert.Equal("test123456abc", result) + + [] + let ``Handles mixed case correctly`` () = + let text = "Hello,World_Test" + let result = stripSeparatorsAndTrim text + + Assert.Equal("HelloWorldTest", result) + + [] + let ``Handles Unicode characters`` () = + let text = "café,naïve_résumé,東京と京都" + let result = stripSeparatorsAndTrim text + + Assert.Equal("cafénaïverésumé東京と京都", result) + + [] + let ``Handles single character input`` () = + let text = "_" + let result = stripSeparatorsAndTrim text + + Assert.Equal(String.Empty, result) + + [] + let ``Handles single character without separator`` () = + let text = "a" + let result = stripSeparatorsAndTrim text + + Assert.Equal("a", result) + + [] + let ``Handles separators at start`` () = + let text = "_,hello" + let result = stripSeparatorsAndTrim text + + Assert.Equal("hello", result) + + [] + let ``Handles separators at end`` () = + let text = "hello_," + let result = stripSeparatorsAndTrim text + + Assert.Equal("hello", result) + + [] + let ``Handles separators at both ends`` () = + let text = ",_hello_," + let result = stripSeparatorsAndTrim text + + Assert.Equal("hello", result) + + [] + let ``Handles large strings`` () = + let text = String.replicate 1000 "a_b," + 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 = stripSeparatorsAndTrim text + + Assert.Equal("helloworld\ttestexample\nmore", result) + diff --git a/src/UniqueFileGenerator.Tests/StringGenerationTests.fs b/src/UniqueFileGenerator.Tests/StringGenerationTests.fs index 75800f6..adb3a42 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 = [] @@ -11,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) [] @@ -28,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) [] @@ -38,7 +39,7 @@ module Strings = [] let ``Throws with negative item length`` () = - Assert.Throws(fun () -> + Assert.Throws(fun () -> (generateMultiple -1 5000) :> obj) module FileNames = @@ -50,21 +51,21 @@ 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) + Assert.Equal(generated, fileNames) [] - let ``Generates filenames with a prefix and no extension`` () = - let prefix = "@@" + let ``Generates filenames with a prefix (respecting spaces) and no extension`` () = + let prefix = " @@ " let extension = String.Empty let generated = generateMultiple 10 10 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 |> List.forall (fun x -> x.StartsWith prefix)) [] let ``Generates filenames with an extension and no prefix`` () = @@ -74,9 +75,9 @@ 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)) + Assert.True(fileNames |> List.forall (fun x -> x.EndsWith extension)) [] let ``Generates filenames with a prefix and extension args`` () = @@ -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)) + Assert.True(fileNames |> List.forall (fun x -> x.StartsWith prefix)) + Assert.True(fileNames |> List.forall (fun x -> x.EndsWith extension)) 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