Skip to content

Commit 494957f

Browse files
Fix: Apply DateTimeFormats and Culture from CsvReaderOptions to DateTimeConverter (#44)
* Fix: Apply DateTimeFormats and Culture from CsvReaderOptions to DateTimeConverter Addresses issue #43 where Import-DbaCsv ignores -DateTimeFormats and -Culture parameters. The issue was in CsvDataReader.CacheColumnConverters() which always used the default DateTimeConverter instance from the registry, which has no custom formats and uses InvariantCulture. Now when DateTimeFormats or Culture are specified in CsvReaderOptions, a custom DateTimeConverter is created with those settings. This ensures dates like '04/02/2026 15:14:21' with format 'dd/MM/yyyy HH:mm:ss' are parsed correctly as February 4th instead of April 2nd. Changes: - Modified CsvDataReader.CacheColumnConverters() to create custom DateTimeConverter instances when DateTimeFormats or Culture are specified in options - Added test TestDateTimeConversionWithCustomFormats() to verify dd/MM/yyyy parsing - Added test TestDateTimeConversionWithCulture() to verify Culture parameter support Co-authored-by: Chrissy LeMaire <potatoqualitee@users.noreply.github.com> * Add comprehensive DateTime conversion tests Added 10 new tests to thoroughly cover DateTimeFormats and Culture parameter handling: - Multiple date format precedence - Combined custom formats with culture settings - NULL and empty value handling - Various date formats (ISO, US, EU, short formats) - Timezone format support - Format precedence for ambiguous dates - Regression test for default behavior These tests ensure the fix in issue #43 properly handles edge cases and various date format scenarios. Co-authored-by: Chrissy LeMaire <potatoqualitee@users.noreply.github.com> * Allow 'claude[bot]' in Claude code-review action Add the allowed_bots input to the anthropics/claude-code-action configuration in the workflow, permitting the claude[bot] account to be recognized as an authorized bot for automated code reviews. No other workflow behavior or secrets were changed. * Allow all bots in Claude code review workflow Update .github/workflows/claude-code-review.yml to change allowed_bots from "claude[bot]" to "*" for the anthropics/claude-code-action. This allows any bot account to be accepted by the action (enabling other bots/integrations to run the code review); consider auditing for security implications if broad bot access is undesirable. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent aecbec9 commit 494957f

File tree

3 files changed

+355
-2
lines changed

3 files changed

+355
-2
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
uses: anthropics/claude-code-action@v1
3737
with:
3838
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39+
allowed_bots: "*"
3940
prompt: |
4041
REPO: ${{ github.repository }}
4142
PR NUMBER: ${{ github.event.pull_request.number }}

project/dbatools.Tests/Csv/CsvDataReaderTest.cs

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,333 @@ public void TestDateTimeConversion()
340340
}
341341
}
342342

343+
[TestMethod]
344+
public void TestDateTimeConversionWithCustomFormats()
345+
{
346+
// Addresses issue #43: Import-DbaCsv ignores -DateTimeFormats switch
347+
// Test that dd/MM/yyyy format is correctly parsed when specified in DateTimeFormats
348+
string csv = "Character Column,Test Date Time,Character Column 2\nTest data,04/02/2026 15:14:21,ABC123\nTest Data2,04/02/2026 15:14:21,MNB675";
349+
var options = new CsvReaderOptions
350+
{
351+
DateTimeFormats = new[] { "dd/MM/yyyy HH:mm:ss" },
352+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
353+
{
354+
{ "Test Date Time", typeof(DateTime) }
355+
}
356+
};
357+
358+
using (var reader = CreateReaderFromString(csv, options))
359+
{
360+
Assert.IsTrue(reader.Read());
361+
DateTime dt = reader.GetDateTime(1);
362+
Assert.AreEqual(2026, dt.Year);
363+
Assert.AreEqual(2, dt.Month, "Month should be February (2), not April (4)");
364+
Assert.AreEqual(4, dt.Day, "Day should be 4");
365+
Assert.AreEqual(15, dt.Hour);
366+
Assert.AreEqual(14, dt.Minute);
367+
Assert.AreEqual(21, dt.Second);
368+
369+
Assert.IsTrue(reader.Read());
370+
dt = reader.GetDateTime(1);
371+
Assert.AreEqual(2026, dt.Year);
372+
Assert.AreEqual(2, dt.Month, "Month should be February (2), not April (4)");
373+
Assert.AreEqual(4, dt.Day, "Day should be 4");
374+
}
375+
}
376+
377+
[TestMethod]
378+
public void TestDateTimeConversionWithCulture()
379+
{
380+
// Test that Culture parameter is respected for DateTime parsing
381+
string csv = "Name,Date\nJohn,04/02/2026";
382+
var options = new CsvReaderOptions
383+
{
384+
Culture = new System.Globalization.CultureInfo("en-GB"),
385+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
386+
{
387+
{ "Date", typeof(DateTime) }
388+
}
389+
};
390+
391+
using (var reader = CreateReaderFromString(csv, options))
392+
{
393+
Assert.IsTrue(reader.Read());
394+
DateTime dt = reader.GetDateTime(1);
395+
Assert.AreEqual(2026, dt.Year);
396+
Assert.AreEqual(2, dt.Month, "With en-GB culture, 04/02/2026 should be February 4th");
397+
Assert.AreEqual(4, dt.Day);
398+
}
399+
}
400+
401+
[TestMethod]
402+
public void TestDateTimeConversionWithMultipleFormats()
403+
{
404+
// Test multiple date formats - should try each format until one succeeds
405+
string csv = "Name,Date1,Date2,Date3\nJohn,2026-02-04,04/02/2026,Feb 4 2026";
406+
var options = new CsvReaderOptions
407+
{
408+
DateTimeFormats = new[]
409+
{
410+
"yyyy-MM-dd", // ISO format
411+
"dd/MM/yyyy", // European format
412+
"MMM d yyyy" // Month name format
413+
},
414+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
415+
{
416+
{ "Date1", typeof(DateTime) },
417+
{ "Date2", typeof(DateTime) },
418+
{ "Date3", typeof(DateTime) }
419+
}
420+
};
421+
422+
using (var reader = CreateReaderFromString(csv, options))
423+
{
424+
Assert.IsTrue(reader.Read());
425+
426+
// All three dates should parse to the same day
427+
DateTime dt1 = reader.GetDateTime(1);
428+
Assert.AreEqual(2026, dt1.Year);
429+
Assert.AreEqual(2, dt1.Month);
430+
Assert.AreEqual(4, dt1.Day);
431+
432+
DateTime dt2 = reader.GetDateTime(2);
433+
Assert.AreEqual(2026, dt2.Year);
434+
Assert.AreEqual(2, dt2.Month);
435+
Assert.AreEqual(4, dt2.Day);
436+
437+
DateTime dt3 = reader.GetDateTime(3);
438+
Assert.AreEqual(2026, dt3.Year);
439+
Assert.AreEqual(2, dt3.Month);
440+
Assert.AreEqual(4, dt3.Day);
441+
}
442+
}
443+
444+
[TestMethod]
445+
public void TestDateTimeConversionWithCustomFormatsAndCulture()
446+
{
447+
// Test combining custom formats with custom culture
448+
// French culture uses different date/time separators and names
449+
string csv = "Name,Date\nPierre,04/02/2026 15:14:21";
450+
var options = new CsvReaderOptions
451+
{
452+
Culture = new System.Globalization.CultureInfo("fr-FR"),
453+
DateTimeFormats = new[] { "dd/MM/yyyy HH:mm:ss" },
454+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
455+
{
456+
{ "Date", typeof(DateTime) }
457+
}
458+
};
459+
460+
using (var reader = CreateReaderFromString(csv, options))
461+
{
462+
Assert.IsTrue(reader.Read());
463+
DateTime dt = reader.GetDateTime(1);
464+
Assert.AreEqual(2026, dt.Year);
465+
Assert.AreEqual(2, dt.Month);
466+
Assert.AreEqual(4, dt.Day);
467+
Assert.AreEqual(15, dt.Hour);
468+
Assert.AreEqual(14, dt.Minute);
469+
Assert.AreEqual(21, dt.Second);
470+
}
471+
}
472+
473+
[TestMethod]
474+
public void TestDateTimeConversionWithNullValue()
475+
{
476+
// Test that NULL values are handled correctly with custom formats
477+
string csv = "Name,Date\nJohn,2026-02-04\nJane,NULL";
478+
var options = new CsvReaderOptions
479+
{
480+
DateTimeFormats = new[] { "yyyy-MM-dd" },
481+
NullValue = "NULL",
482+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
483+
{
484+
{ "Date", typeof(DateTime?) }
485+
}
486+
};
487+
488+
using (var reader = CreateReaderFromString(csv, options))
489+
{
490+
Assert.IsTrue(reader.Read());
491+
Assert.IsFalse(reader.IsDBNull(1));
492+
DateTime dt = reader.GetDateTime(1);
493+
Assert.AreEqual(2026, dt.Year);
494+
Assert.AreEqual(2, dt.Month);
495+
Assert.AreEqual(4, dt.Day);
496+
497+
Assert.IsTrue(reader.Read());
498+
Assert.IsTrue(reader.IsDBNull(1), "NULL value should be DBNull");
499+
}
500+
}
501+
502+
[TestMethod]
503+
public void TestDateTimeConversionWithEmptyValue()
504+
{
505+
// Test that empty values are treated as DBNull
506+
string csv = "Name,Date\nJohn,2026-02-04\nJane,";
507+
var options = new CsvReaderOptions
508+
{
509+
DateTimeFormats = new[] { "yyyy-MM-dd" },
510+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
511+
{
512+
{ "Date", typeof(DateTime?) }
513+
}
514+
};
515+
516+
using (var reader = CreateReaderFromString(csv, options))
517+
{
518+
Assert.IsTrue(reader.Read());
519+
Assert.IsFalse(reader.IsDBNull(1));
520+
521+
Assert.IsTrue(reader.Read());
522+
Assert.IsTrue(reader.IsDBNull(1), "Empty value should be DBNull");
523+
}
524+
}
525+
526+
[TestMethod]
527+
public void TestDateTimeConversionFormatPrecedence()
528+
{
529+
// Test that formats are tried in order and first match wins
530+
// Ambiguous date 01/02/2026 could be Jan 2 or Feb 1
531+
string csv = "Name,Date\nTest,01/02/2026";
532+
var options = new CsvReaderOptions
533+
{
534+
// First format is MM/dd/yyyy (US), second is dd/MM/yyyy (EU)
535+
DateTimeFormats = new[] { "MM/dd/yyyy", "dd/MM/yyyy" },
536+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
537+
{
538+
{ "Date", typeof(DateTime) }
539+
}
540+
};
541+
542+
using (var reader = CreateReaderFromString(csv, options))
543+
{
544+
Assert.IsTrue(reader.Read());
545+
DateTime dt = reader.GetDateTime(1);
546+
// Should parse as January 2nd (first format)
547+
Assert.AreEqual(2026, dt.Year);
548+
Assert.AreEqual(1, dt.Month, "Should use first format MM/dd/yyyy, so month is January (1)");
549+
Assert.AreEqual(2, dt.Day);
550+
}
551+
}
552+
553+
[TestMethod]
554+
public void TestDateTimeConversionWithTimeZoneFormat()
555+
{
556+
// Test ISO 8601 format with time zone
557+
string csv = "Name,Timestamp\nJohn,2026-02-04T15:14:21Z";
558+
var options = new CsvReaderOptions
559+
{
560+
DateTimeFormats = new[] { "yyyy-MM-ddTHH:mm:ssZ" },
561+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
562+
{
563+
{ "Timestamp", typeof(DateTime) }
564+
}
565+
};
566+
567+
using (var reader = CreateReaderFromString(csv, options))
568+
{
569+
Assert.IsTrue(reader.Read());
570+
DateTime dt = reader.GetDateTime(1);
571+
Assert.AreEqual(2026, dt.Year);
572+
Assert.AreEqual(2, dt.Month);
573+
Assert.AreEqual(4, dt.Day);
574+
Assert.AreEqual(15, dt.Hour);
575+
Assert.AreEqual(14, dt.Minute);
576+
Assert.AreEqual(21, dt.Second);
577+
}
578+
}
579+
580+
[TestMethod]
581+
public void TestDateTimeConversionWithUSCulture()
582+
{
583+
// Test US culture with default parsing (MM/dd/yyyy)
584+
string csv = "Name,Date\nJohn,02/04/2026";
585+
var options = new CsvReaderOptions
586+
{
587+
Culture = new System.Globalization.CultureInfo("en-US"),
588+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
589+
{
590+
{ "Date", typeof(DateTime) }
591+
}
592+
};
593+
594+
using (var reader = CreateReaderFromString(csv, options))
595+
{
596+
Assert.IsTrue(reader.Read());
597+
DateTime dt = reader.GetDateTime(1);
598+
Assert.AreEqual(2026, dt.Year);
599+
Assert.AreEqual(2, dt.Month, "With en-US culture, 02/04/2026 should be February 4th");
600+
Assert.AreEqual(4, dt.Day);
601+
}
602+
}
603+
604+
[TestMethod]
605+
public void TestDateTimeConversionWithShortDateFormat()
606+
{
607+
// Test various short date formats
608+
string csv = "Name,Date1,Date2,Date3\nJohn,2026-2-4,2026.02.04,20260204";
609+
var options = new CsvReaderOptions
610+
{
611+
DateTimeFormats = new[]
612+
{
613+
"yyyy-M-d", // No leading zeros
614+
"yyyy.MM.dd", // Dot separator
615+
"yyyyMMdd" // No separators
616+
},
617+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
618+
{
619+
{ "Date1", typeof(DateTime) },
620+
{ "Date2", typeof(DateTime) },
621+
{ "Date3", typeof(DateTime) }
622+
}
623+
};
624+
625+
using (var reader = CreateReaderFromString(csv, options))
626+
{
627+
Assert.IsTrue(reader.Read());
628+
629+
DateTime dt1 = reader.GetDateTime(1);
630+
Assert.AreEqual(2026, dt1.Year);
631+
Assert.AreEqual(2, dt1.Month);
632+
Assert.AreEqual(4, dt1.Day);
633+
634+
DateTime dt2 = reader.GetDateTime(2);
635+
Assert.AreEqual(2026, dt2.Year);
636+
Assert.AreEqual(2, dt2.Month);
637+
Assert.AreEqual(4, dt2.Day);
638+
639+
DateTime dt3 = reader.GetDateTime(3);
640+
Assert.AreEqual(2026, dt3.Year);
641+
Assert.AreEqual(2, dt3.Month);
642+
Assert.AreEqual(4, dt3.Day);
643+
}
644+
}
645+
646+
[TestMethod]
647+
public void TestDateTimeConversionWithoutCustomFormats()
648+
{
649+
// Verify that without custom formats, standard parsing still works
650+
string csv = "Name,Date\nJohn,2026-02-04T15:14:21";
651+
var options = new CsvReaderOptions
652+
{
653+
// No DateTimeFormats specified - should use default converter
654+
ColumnTypes = new System.Collections.Generic.Dictionary<string, Type>
655+
{
656+
{ "Date", typeof(DateTime) }
657+
}
658+
};
659+
660+
using (var reader = CreateReaderFromString(csv, options))
661+
{
662+
Assert.IsTrue(reader.Read());
663+
DateTime dt = reader.GetDateTime(1);
664+
Assert.AreEqual(2026, dt.Year);
665+
Assert.AreEqual(2, dt.Month);
666+
Assert.AreEqual(4, dt.Day);
667+
}
668+
}
669+
343670
[TestMethod]
344671
public void TestNumericConversion()
345672
{

project/dbatools/Csv/Reader/CsvDataReader.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,8 +496,33 @@ private void CacheColumnConverters()
496496
if (column.DataType == typeof(string))
497497
continue;
498498

499-
// Use custom converter if specified, otherwise look up from registry
500-
column.CachedConverter = column.Converter ?? _options.TypeConverterRegistry?.GetConverter(column.DataType);
499+
// Use custom converter if specified
500+
if (column.Converter != null)
501+
{
502+
column.CachedConverter = column.Converter;
503+
continue;
504+
}
505+
506+
// For DateTime columns, check if we need a custom converter with DateTimeFormats/Culture
507+
if (column.DataType == typeof(DateTime) || column.DataType == typeof(DateTime?))
508+
{
509+
bool hasCustomFormats = _options.DateTimeFormats != null && _options.DateTimeFormats.Length > 0;
510+
bool hasCustomCulture = _options.Culture != null && !_options.Culture.Equals(CultureInfo.InvariantCulture);
511+
512+
if (hasCustomFormats || hasCustomCulture)
513+
{
514+
// Create a custom DateTimeConverter with the specified formats and culture
515+
column.CachedConverter = new DateTimeConverter
516+
{
517+
CustomFormats = _options.DateTimeFormats,
518+
Culture = _options.Culture ?? CultureInfo.InvariantCulture
519+
};
520+
continue;
521+
}
522+
}
523+
524+
// Fall back to registry default converter
525+
column.CachedConverter = _options.TypeConverterRegistry?.GetConverter(column.DataType);
501526
}
502527
}
503528

0 commit comments

Comments
 (0)