Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System.Globalization;
using Cosmos.DataTransfer.JsonExtension.UnitTests;
using Microsoft.Extensions.Logging.Abstractions;
using System.ComponentModel.DataAnnotations;
using Cosmos.DataTransfer.CsvExtension.Settings;
using Cosmos.DataTransfer.Interfaces;
using Cosmos.DataTransfer.JsonExtension;

namespace Cosmos.DataTransfer.CsvExtension.UnitTests;

[TestClass]
public class CsvWriterSettingsTests
{


[TestMethod]
public void TestDefault() {
var settings = new CsvWriterSettings() { };

Assert.AreEqual(CultureInfo.InvariantCulture, settings.GetCultureInfo());
Assert.AreEqual(0, settings.Validate(new ValidationContext(this)).Count());
}

[TestMethod]
[DataRow("invariant")]
[DataRow("Invariant")]
[DataRow("invariantCulture")]
[DataRow("invariantculture")]
public void TestInvariantCulture(string culture) {
var settings = new CsvWriterSettings() {
Culture = culture
};
Assert.AreEqual(CultureInfo.InvariantCulture, settings.GetCultureInfo());
Assert.AreEqual(0, settings.Validate(new ValidationContext(this)).Count());
}

[TestMethod]
[DataRow("current")]
[DataRow("Current")]
[DataRow("currentCultuRE")]
[DataRow("currentCulture")]
public void TestCurrentCulture(string culture) {
var settings = new CsvWriterSettings() {
Culture = culture
};
Assert.AreEqual(CultureInfo.CurrentCulture, settings.GetCultureInfo());
Assert.AreEqual(0, settings.Validate(new ValidationContext(this)).Count());
}

[TestMethod]
public void TestCurrentCultureByName() {
if (string.IsNullOrEmpty(CultureInfo.CurrentCulture.Name))
{
Assert.Inconclusive("Current culture name in executing environment is empty.");
}

var settings = new CsvWriterSettings() {
Culture = CultureInfo.CurrentCulture.Name
};
Assert.AreEqual(CultureInfo.CurrentCulture, settings.GetCultureInfo());
Assert.AreEqual(0, settings.Validate(new ValidationContext(this)).Count());
}

[TestMethod]
public void TestCultureFails() {
var settings = new CsvWriterSettings() {
Culture = "not a culture"
};
var results = settings.Validate(new ValidationContext(this)).ToArray();
Assert.AreEqual(1, results.Count());
Assert.AreEqual("Could not find CultureInfo `not a culture` on this system.", results.First().ErrorMessage);
}

[TestMethod]
public void TestCultureMissing() {
var settings = new CsvWriterSettings() {
Culture = ""
};
var results = settings.Validate(new ValidationContext(this)).ToArray();
Assert.AreEqual(1, results.Count());
Assert.AreEqual("Culture missing.", results.First().ErrorMessage);
}

[TestMethod]
public void TestCultureNull()
{
var settings = new CsvWriterSettings()
{
Culture = null
};
var results = settings.Validate(new ValidationContext(this)).ToArray();
Assert.AreEqual(1, results.Count());
Assert.AreEqual("Culture missing.", results.First().ErrorMessage);
}

[TestMethod]
public async Task TestDanishCulture() {
var outputFile = Path.GetTempFileName();
var config = TestHelpers.CreateConfig(new Dictionary<string, string>
{
{ "FilePath", outputFile },
{ "IncludeHeader", "false" },
{ "Culture", "da-DK" },
{ "Delimiter", ";" }
});

var data = new List<DictionaryDataItem>
{
new(new Dictionary<string, object?>
{
{ "Value", 1.2 }
})
};

var sink = new CsvFileSink();

await sink.WriteAsync(data.ToAsyncEnumerable(), config, new JsonFileSource(), NullLogger.Instance);
var result = await File.ReadAllTextAsync(outputFile);
Assert.AreEqual("1,2", result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ public async Task FormatDataAsync(IAsyncEnumerable<IDataItem> dataItems, Stream
settings.Validate();

await using var textWriter = new StreamWriter(target, leaveOpen: true);
await using var writer = new CsvWriter(textWriter, new CsvConfiguration(CultureInfo.InvariantCulture)
await using var writer = new CsvWriter(textWriter, new CsvConfiguration(settings.GetCultureInfo())
{
Delimiter = settings.Delimiter,
Delimiter = settings.Delimiter ?? ",",
HasHeaderRecord = settings.IncludeHeader,
});

Expand All @@ -49,7 +49,7 @@ public async Task FormatDataAsync(IAsyncEnumerable<IDataItem> dataItems, Stream

foreach (string field in item.GetFieldNames())
{
writer.WriteField(item.GetValue(field)?.ToString());
writer.WriteField(item.GetValue(field));
}

firstRecord = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,50 @@
using Cosmos.DataTransfer.Interfaces;
using System.Globalization;
using Cosmos.DataTransfer.Interfaces;
using System.ComponentModel.DataAnnotations;

namespace Cosmos.DataTransfer.CsvExtension.Settings;

public class CsvWriterSettings : IDataExtensionSettings
public class CsvWriterSettings : IDataExtensionSettings, IValidatableObject
{
public bool IncludeHeader { get; set; } = true;
public string Delimiter { get; set; } = ",";
public string? Delimiter { get; set; } = ",";
public string? Culture { get; set; } = "InvariantCulture";
public CultureInfo GetCultureInfo() {
switch (this.Culture?.ToLower())
{
case "invariant":
case "invariantculture":
return CultureInfo.InvariantCulture;
case "current":
case "currentculture":
return CultureInfo.CurrentCulture;
case "":
case null:
throw new ArgumentNullException();
default: return CultureInfo.GetCultureInfo(this.Culture!);
}
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
ValidationResult? result = null;
try {
_ = this.GetCultureInfo();
} catch (CultureNotFoundException) {
result = new ValidationResult(
$"Could not find CultureInfo `{this.Culture}` on this system.",
new string[] { "Culture" }
);
} catch (ArgumentNullException) {
result = new ValidationResult(
$"Culture missing.",
new string[] { "Culture" }
);
}


if (result != null) {
yield return result;
}
}
}
13 changes: 12 additions & 1 deletion Extensions/Csv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ See storage extension documentation for any storage specific settings needed ([e

Source supports an optional `Delimiter` parameter (`,` by default) and an optional `HasHeader` parameter (`true` by default). For files without a header, column names will be generated based on the `ColumnNameFormat` setting, which uses a default value of `column_{0}` to produce columns `column_0`, `column_1`, etc.


```json
{
"Delimiter": ",",
Expand All @@ -35,9 +36,19 @@ Source supports an optional `Delimiter` parameter (`,` by default) and an option

Sink supports an optional `Delimiter` parameter (`,` by default) and an optional `IncludeHeader` parameter (`true` by default) to add a leading row of column names.

Formatting options, or locale, can be set with an optional `Culture` setting (`"InvariantCulture"` by default).
This specifies how e.g., numbers and dates are formatted according to a specific culture.
Set to `"InvariantCulture"` to use the system's or process' current locale setting
(see [CultureInfo.CurrentCulture](https://learn.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.currentculture)),
or e.g., `"en"`, `"en-GB"`, or `"en-US"` for English standards (period, `.`, as decimal separator and other regional standards),
"da-DK" for Danish (comma, `,`, as decimal separator), etc.
Note, if using a culture with comma as decimal separator, specify a different delimiter (e.g., semi-colon, `;`), else all numbers
will be written enclosed with quotes.

```json
{
"Delimiter": ",",
"IncludeHeader": true
"IncludeHeader": true,
"Culture": "Invariant"
}
```
Loading