1+ // --------------------------------------------------------------------------------------
2+ // Tests for the CsvFile and CsvRow functionality
3+ // --------------------------------------------------------------------------------------
4+
5+ module FSharp.Data.Tests.CsvFile
6+
7+ open NUnit.Framework
8+ open FsUnit
9+ open System.IO
10+ open FSharp.Data
11+ open FSharp.Data .CsvExtensions
12+
13+ // Sample CSV data for testing
14+ let sampleCsvData = """ Name,Age,City
15+ John,25,New York
16+ Jane,30,London
17+ Bob,35,Paris"""
18+
19+ let sampleCsvNoHeaders = """ John,25,New York
20+ Jane,30,London
21+ Bob,35,Paris"""
22+
23+ let sampleCsvWithQuotes = """ Name,Description,Score
24+ Alice,"Software Engineer, Senior",95
25+ Bob,"Product Manager""s Assistant",88
26+ Charlie,"Data Scientist
27+ with ML focus",92"""
28+
29+ [<Test>]
30+ let ``CsvFile.Parse works with headers`` () =
31+ let csv = CsvFile.Parse( sampleCsvData)
32+
33+ csv.Headers |> should equal ( Some [| " Name" ; " Age" ; " City" |])
34+ csv.NumberOfColumns |> should equal 3
35+ let rows = csv.Rows |> Array.ofSeq
36+ rows.Length |> should equal 3
37+ rows.[ 0 ]. Columns |> should equal [| " John" ; " 25" ; " New York" |]
38+
39+ [<Test>]
40+ let ``CsvFile.Parse works without headers`` () =
41+ let csv = CsvFile.Parse( sampleCsvNoHeaders, hasHeaders= false )
42+
43+ csv.Headers |> should equal None
44+ csv.NumberOfColumns |> should equal 3
45+ let rows = csv.Rows |> Array.ofSeq
46+ rows.Length |> should equal 3
47+ rows.[ 0 ]. Columns |> should equal [| " John" ; " 25" ; " New York" |]
48+
49+ [<Test>]
50+ let ``CsvFile.Parse handles custom separators`` () =
51+ let csvData = " Name;Age;City\n John;25;New York"
52+ let csv = CsvFile.Parse( csvData, separators= " ;" )
53+
54+ csv.Headers |> should equal ( Some [| " Name" ; " Age" ; " City" |])
55+ let row = csv.Rows |> Seq.head
56+ row.Columns |> should equal [| " John" ; " 25" ; " New York" |]
57+
58+ [<Test>]
59+ let ``CsvFile.Parse handles custom quotes`` () =
60+ let csvData = " Name,Description\n Alice,'Software Engineer, Senior'"
61+ let csv = CsvFile.Parse( csvData, quote= ''' )
62+
63+ let row = csv.Rows |> Seq.head
64+ row.Columns |> should equal [| " Alice" ; " Software Engineer, Senior" |]
65+
66+ [<Test>]
67+ let ``CsvFile.Parse handles skipRows`` () =
68+ let csvData = """ Comment line 1
69+ Comment line 2
70+ Name,Age
71+ John,25
72+ Jane,30"""
73+ let csv = CsvFile.Parse( csvData, skipRows= 2 )
74+
75+ csv.Headers |> should equal ( Some [| " Name" ; " Age" |])
76+ let rows = csv.Rows |> Array.ofSeq
77+ rows.Length |> should equal 2
78+ rows.[ 0 ]. Columns |> should equal [| " John" ; " 25" |]
79+
80+ [<Test>]
81+ let ``CsvFile.Parse handles ignoreErrors`` () =
82+ let csvData = """ Name,Age,City
83+ John,25,New York
84+ Jane,30 // This row has only 2 columns
85+ Bob,35,Paris"""
86+ let csv = CsvFile.Parse( csvData, ignoreErrors= true )
87+
88+ let rows = csv.Rows |> Array.ofSeq
89+ // Should ignore the malformed row
90+ rows.Length |> should equal 2
91+ rows.[ 0 ]. Columns |> should equal [| " John" ; " 25" ; " New York" |]
92+ rows.[ 1 ]. Columns |> should equal [| " Bob" ; " 35" ; " Paris" |]
93+
94+ [<Test>]
95+ let ``CsvFile.Load from stream works`` () =
96+ use stream = new MemoryStream( System.Text.Encoding.UTF8.GetBytes( sampleCsvData))
97+ let csv = CsvFile.Load( stream)
98+
99+ csv.Headers |> should equal ( Some [| " Name" ; " Age" ; " City" |])
100+ let rows = csv.Rows |> Array.ofSeq
101+ rows.Length |> should equal 3
102+
103+ [<Test>]
104+ let ``CsvFile.Load from TextReader works`` () =
105+ use reader = new StringReader( sampleCsvData)
106+ let csv = CsvFile.Load( reader)
107+
108+ csv.Headers |> should equal ( Some [| " Name" ; " Age" ; " City" |])
109+ let rows = csv.Rows |> Array.ofSeq
110+ rows.Length |> should equal 3
111+
112+ [<Test>]
113+ let ``GetColumnIndex returns correct index for valid column`` () =
114+ let csv = CsvFile.Parse( sampleCsvData)
115+
116+ csv.GetColumnIndex( " Name" ) |> should equal 0
117+ csv.GetColumnIndex( " Age" ) |> should equal 1
118+ csv.GetColumnIndex( " City" ) |> should equal 2
119+
120+ [<Test>]
121+ let ``GetColumnIndex throws for invalid column`` () =
122+ let csv = CsvFile.Parse( sampleCsvData)
123+
124+ Assert.Throws< System.Collections.Generic.KeyNotFoundException>( fun () -> csv.GetColumnIndex( " InvalidColumn" ) |> ignore) |> ignore
125+
126+ [<Test>]
127+ let ``TryGetColumnIndex returns Some for valid column`` () =
128+ let csv = CsvFile.Parse( sampleCsvData)
129+
130+ csv.TryGetColumnIndex( " Name" ) |> should equal ( Some 0 )
131+ csv.TryGetColumnIndex( " Age" ) |> should equal ( Some 1 )
132+ csv.TryGetColumnIndex( " City" ) |> should equal ( Some 2 )
133+
134+ [<Test>]
135+ let ``TryGetColumnIndex returns None for invalid column`` () =
136+ let csv = CsvFile.Parse( sampleCsvData)
137+
138+ csv.TryGetColumnIndex( " InvalidColumn" ) |> should equal None
139+
140+ [<Test>]
141+ let ``CsvRow GetColumn by index works`` () =
142+ let csv = CsvFile.Parse( sampleCsvData)
143+ let row = csv.Rows |> Seq.head
144+
145+ row.GetColumn( 0 ) |> should equal " John"
146+ row.GetColumn( 1 ) |> should equal " 25"
147+ row.GetColumn( 2 ) |> should equal " New York"
148+
149+ [<Test>]
150+ let ``CsvRow GetColumn by name works`` () =
151+ let csv = CsvFile.Parse( sampleCsvData)
152+ let row = csv.Rows |> Seq.head
153+
154+ row.GetColumn( " Name" ) |> should equal " John"
155+ row.GetColumn( " Age" ) |> should equal " 25"
156+ row.GetColumn( " City" ) |> should equal " New York"
157+
158+ [<Test>]
159+ let ``CsvRow indexer by int works`` () =
160+ let csv = CsvFile.Parse( sampleCsvData)
161+ let row = csv.Rows |> Seq.head
162+
163+ row.[ 0 ] |> should equal " John"
164+ row.[ 1 ] |> should equal " 25"
165+ row.[ 2 ] |> should equal " New York"
166+
167+ [<Test>]
168+ let ``CsvRow indexer by string works`` () =
169+ let csv = CsvFile.Parse( sampleCsvData)
170+ let row = csv.Rows |> Seq.head
171+
172+ row.[ " Name" ] |> should equal " John"
173+ row.[ " Age" ] |> should equal " 25"
174+ row.[ " City" ] |> should equal " New York"
175+
176+ [<Test>]
177+ let ``CsvRow dynamic operator works`` () =
178+ let csv = CsvFile.Parse( sampleCsvData)
179+ let row = csv.Rows |> Seq.head
180+
181+ row?Name |> should equal " John"
182+ row?Age |> should equal " 25"
183+ row?City |> should equal " New York"
184+
185+ // Tests for StringExtensions
186+ [<Test>]
187+ let ``StringExtensions.AsInteger works with valid input`` () =
188+ " 123" .AsInteger() |> should equal 123
189+ " -456" .AsInteger() |> should equal - 456
190+
191+ [<Test>]
192+ let ``StringExtensions.AsInteger throws with invalid input`` () =
193+ Assert.Throws< System.Exception>( fun () -> " not a number" .AsInteger() |> ignore) |> ignore
194+
195+ [<Test>]
196+ let ``StringExtensions.AsInteger64 works with valid input`` () =
197+ " 123456789012345" .AsInteger64() |> should equal 123456789012345 L
198+ " -987654321098765" .AsInteger64() |> should equal - 987654321098765 L
199+
200+ [<Test>]
201+ let ``StringExtensions.AsInteger64 throws with invalid input`` () =
202+ Assert.Throws< System.Exception>( fun () -> " not a number" .AsInteger64() |> ignore) |> ignore
203+
204+ [<Test>]
205+ let ``StringExtensions.AsDecimal works with valid input`` () =
206+ " 123.45" .AsDecimal() |> should equal 123.45 m
207+ " -67.89" .AsDecimal() |> should equal - 67.89 m
208+
209+ [<Test>]
210+ let ``StringExtensions.AsDecimal throws with invalid input`` () =
211+ Assert.Throws< System.Exception>( fun () -> " not a decimal" .AsDecimal() |> ignore) |> ignore
212+
213+ [<Test>]
214+ let ``StringExtensions.AsFloat works with valid input`` () =
215+ " 123.45" .AsFloat() |> should equal 123.45
216+ " -67.89" .AsFloat() |> should equal - 67.89
217+
218+ [<Test>]
219+ let ``StringExtensions.AsFloat throws with invalid input`` () =
220+ Assert.Throws< System.Exception>( fun () -> " not a float" .AsFloat() |> ignore) |> ignore
221+
222+ [<Test>]
223+ let ``StringExtensions.AsBoolean works with valid input`` () =
224+ " true" .AsBoolean() |> should equal true
225+ " false" .AsBoolean() |> should equal false
226+ " True" .AsBoolean() |> should equal true
227+ " False" .AsBoolean() |> should equal false
228+ " 1" .AsBoolean() |> should equal true
229+ " 0" .AsBoolean() |> should equal false
230+
231+ [<Test>]
232+ let ``StringExtensions.AsBoolean throws with invalid input`` () =
233+ Assert.Throws< System.Exception>( fun () -> " not a boolean" .AsBoolean() |> ignore) |> ignore
234+
235+ [<Test>]
236+ let ``StringExtensions.AsDateTime works with valid input`` () =
237+ let result = " 2023-12-25" .AsDateTime()
238+ result.Year |> should equal 2023
239+ result.Month |> should equal 12
240+ result.Day |> should equal 25
241+
242+ [<Test>]
243+ let ``StringExtensions.AsDateTime throws with invalid input`` () =
244+ Assert.Throws< System.Exception>( fun () -> " not a date" .AsDateTime() |> ignore) |> ignore
245+
246+ [<Test>]
247+ let ``StringExtensions.AsGuid works with valid input`` () =
248+ let guidString = " 12345678-1234-1234-1234-123456789abc"
249+ let result = guidString.AsGuid()
250+ result.ToString() |> should equal guidString
251+
252+ [<Test>]
253+ let ``StringExtensions.AsGuid throws with invalid input`` () =
254+ Assert.Throws< System.Exception>( fun () -> " not a guid" .AsGuid() |> ignore) |> ignore
255+
256+ [<Test>]
257+ let ``Complex CSV with quotes and newlines handled correctly`` () =
258+ let csv = CsvFile.Parse( sampleCsvWithQuotes)
259+ let rows = csv.Rows |> Array.ofSeq
260+
261+ rows.Length |> should equal 3
262+ rows.[ 0 ].[ " Name" ] |> should equal " Alice"
263+ rows.[ 0 ].[ " Description" ] |> should equal " Software Engineer, Senior"
264+ rows.[ 1 ].[ " Description" ] |> should equal " Product Manager\" s Assistant"
265+ rows.[ 2 ].[ " Description" ] |> should equal " Data Scientist\n with ML focus"
0 commit comments