Skip to content

Commit 4b84053

Browse files
committed
2 parents e38b293 + c31bc35 commit 4b84053

4 files changed

Lines changed: 551 additions & 13 deletions

File tree

src/FSharp.Data.Json.Core/JsonValue.fs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,53 @@ type JsonValue =
125125

126126
serialize 0 x
127127

128+
// Optimized JSON string encoding with reduced allocations and bulk writing
128129
// Encode characters that are not valid in JS string. The implementation is based
129130
// on https://github.com/mono/mono/blob/master/mcs/class/System.Web/System.Web/HttpUtility.cs
130131
static member internal JsonStringEncodeTo (w: TextWriter) (value: string) =
131132
if not (String.IsNullOrEmpty value) then
133+
let mutable lastWritePos = 0
134+
132135
for i = 0 to value.Length - 1 do
133136
let c = value.[i]
134137
let ci = int c
135138

136-
if ci >= 0 && ci <= 7 || ci = 11 || ci >= 14 && ci <= 31 then
137-
w.Write("\\u{0:x4}", ci) |> ignore
138-
else
139-
match c with
140-
| '\b' -> w.Write "\\b"
141-
| '\t' -> w.Write "\\t"
142-
| '\n' -> w.Write "\\n"
143-
| '\f' -> w.Write "\\f"
144-
| '\r' -> w.Write "\\r"
145-
| '"' -> w.Write "\\\""
146-
| '\\' -> w.Write "\\\\"
147-
| _ -> w.Write c
139+
let needsEscaping =
140+
ci >= 0 && ci <= 7
141+
|| ci = 11
142+
|| ci >= 14 && ci <= 31
143+
|| c = '\b'
144+
|| c = '\t'
145+
|| c = '\n'
146+
|| c = '\f'
147+
|| c = '\r'
148+
|| c = '"'
149+
|| c = '\\'
150+
151+
if needsEscaping then
152+
// Write all accumulated unescaped characters in one operation using Substring
153+
if i > lastWritePos then
154+
w.Write(value.Substring(lastWritePos, i - lastWritePos))
155+
156+
// Write the escaped character
157+
if ci >= 0 && ci <= 7 || ci = 11 || ci >= 14 && ci <= 31 then
158+
w.Write("\\u{0:x4}", ci) |> ignore
159+
else
160+
match c with
161+
| '\b' -> w.Write "\\b"
162+
| '\t' -> w.Write "\\t"
163+
| '\n' -> w.Write "\\n"
164+
| '\f' -> w.Write "\\f"
165+
| '\r' -> w.Write "\\r"
166+
| '"' -> w.Write "\\\""
167+
| '\\' -> w.Write "\\\\"
168+
| _ -> w.Write c
169+
170+
lastWritePos <- i + 1
171+
172+
// Write any remaining unescaped characters
173+
if lastWritePos < value.Length then
174+
w.Write(value.Substring(lastWritePos))
148175

149176
member x.ToString(saveOptions, ?indentationSpaces: int) =
150177
let w = new StringWriter(CultureInfo.InvariantCulture)

tests/FSharp.Data.Core.Tests/CsvFile.fs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,94 @@ let ``Complex CSV with quotes and newlines handled correctly`` () =
262262
rows.[0].["Name"] |> should equal "Alice"
263263
rows.[0].["Description"] |> should equal "Software Engineer, Senior"
264264
rows.[1].["Description"] |> should equal "Product Manager\"s Assistant"
265-
rows.[2].["Description"] |> should equal "Data Scientist\nwith ML focus"
265+
rows.[2].["Description"] |> should equal "Data Scientist\nwith ML focus"
266+
267+
// Sample CSV data with time/date fields for testing extension methods
268+
let csvWithDateTimeData = """StartDate,EndDate,Duration,Offset
269+
2023-01-15,2023-01-20,5.00:00:00,2023-01-15T10:30:00+02:00
270+
2023-02-01,2023-02-03,2.12:30:45,2023-02-01T14:15:30-05:00
271+
2023-03-10,2023-03-15,4.08:15:20,2023-03-10T09:45:00+00:00"""
272+
273+
// StringExtensions tests for CSV context - Missing coverage area
274+
[<Test>]
275+
let ``StringExtensions.AsTimeSpan works with valid input`` () =
276+
let csv = CsvFile.Parse(csvWithDateTimeData)
277+
let firstRow = csv.Rows |> Seq.head
278+
279+
firstRow.["Duration"].AsTimeSpan() |> should equal (System.TimeSpan(5, 0, 0, 0))
280+
281+
[<Test>]
282+
let ``StringExtensions.AsTimeSpan works with complex time format`` () =
283+
let csv = CsvFile.Parse(csvWithDateTimeData)
284+
let rows = csv.Rows |> Array.ofSeq
285+
286+
rows.[1].["Duration"].AsTimeSpan() |> should equal (System.TimeSpan(2, 12, 30, 45))
287+
rows.[2].["Duration"].AsTimeSpan() |> should equal (System.TimeSpan(4, 8, 15, 20))
288+
289+
[<Test>]
290+
let ``StringExtensions.AsTimeSpan throws with invalid input`` () =
291+
let csv = CsvFile.Parse("InvalidTime\ninvalid_time")
292+
let row = csv.Rows |> Seq.head
293+
294+
Assert.Throws<System.Exception>(fun () -> row.["InvalidTime"].AsTimeSpan() |> ignore) |> ignore
295+
296+
[<Test>]
297+
let ``StringExtensions.AsDateTimeOffset works with valid input`` () =
298+
let csv = CsvFile.Parse(csvWithDateTimeData)
299+
let firstRow = csv.Rows |> Seq.head
300+
301+
let result = firstRow.["Offset"].AsDateTimeOffset()
302+
result.DateTime |> should equal (System.DateTime(2023, 1, 15, 10, 30, 0))
303+
result.Offset |> should equal (System.TimeSpan(2, 0, 0))
304+
305+
[<Test>]
306+
let ``StringExtensions.AsDateTimeOffset works with negative offset`` () =
307+
let csv = CsvFile.Parse(csvWithDateTimeData)
308+
let rows = csv.Rows |> Array.ofSeq
309+
310+
let result = rows.[1].["Offset"].AsDateTimeOffset()
311+
result.DateTime |> should equal (System.DateTime(2023, 2, 1, 14, 15, 30))
312+
result.Offset |> should equal (System.TimeSpan(-5, 0, 0))
313+
314+
[<Test>]
315+
let ``StringExtensions.AsDateTimeOffset works with zero offset`` () =
316+
let csv = CsvFile.Parse(csvWithDateTimeData)
317+
let rows = csv.Rows |> Array.ofSeq
318+
319+
let result = rows.[2].["Offset"].AsDateTimeOffset()
320+
result.DateTime |> should equal (System.DateTime(2023, 3, 10, 9, 45, 0))
321+
result.Offset |> should equal (System.TimeSpan.Zero)
322+
323+
[<Test>]
324+
let ``StringExtensions.AsDateTimeOffset throws with invalid input`` () =
325+
let csv = CsvFile.Parse("InvalidOffset\ninvalid_offset")
326+
let row = csv.Rows |> Seq.head
327+
328+
Assert.Throws<System.Exception>(fun () -> row.["InvalidOffset"].AsDateTimeOffset() |> ignore) |> ignore
329+
330+
[<Test>]
331+
let ``StringExtensions.AsTimeSpan with custom culture`` () =
332+
let csv = CsvFile.Parse("Duration\n01:30:45")
333+
let row = csv.Rows |> Seq.head
334+
335+
let result = row.["Duration"].AsTimeSpan(System.Globalization.CultureInfo.InvariantCulture)
336+
result |> should equal (System.TimeSpan(1, 30, 45))
337+
338+
[<Test>]
339+
let ``StringExtensions.AsDateTimeOffset with custom culture`` () =
340+
let csv = CsvFile.Parse("Timestamp\n2023-06-15T16:20:30+03:00")
341+
let row = csv.Rows |> Seq.head
342+
343+
let result = row.["Timestamp"].AsDateTimeOffset(System.Globalization.CultureInfo.InvariantCulture)
344+
result.DateTime |> should equal (System.DateTime(2023, 6, 15, 16, 20, 30))
345+
result.Offset |> should equal (System.TimeSpan(3, 0, 0))
346+
347+
[<Test>]
348+
let ``StringExtensions methods work with dynamic operator`` () =
349+
let csv = CsvFile.Parse(csvWithDateTimeData)
350+
let row = csv.Rows |> Seq.head
351+
352+
// Test that the ? operator integrates properly with extension methods
353+
row?Duration.AsTimeSpan() |> should equal (System.TimeSpan(5, 0, 0, 0))
354+
let offsetResult = row?Offset.AsDateTimeOffset()
355+
offsetResult.Offset |> should equal (System.TimeSpan(2, 0, 0))

tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<Compile Include="JsonParserProperties.fs" />
3030
<Compile Include="JsonConversions.fs" />
3131
<Compile Include="JsonRuntime.fs" />
32+
<Compile Include="JsonSchema.fs" />
3233
<Compile Include="CsvReader.fs" />
3334
<Compile Include="CsvFile.fs" />
3435
<Compile Include="HtmlCharRefs.fs" />

0 commit comments

Comments
 (0)