diff --git a/src/Gemstone.COMTRADE/AnalogChannel.cs b/src/Gemstone.COMTRADE/AnalogChannel.cs index e56e9e5..2d9d00d 100644 --- a/src/Gemstone.COMTRADE/AnalogChannel.cs +++ b/src/Gemstone.COMTRADE/AnalogChannel.cs @@ -153,21 +153,21 @@ public AnalogChannel(string lineImage, int version = 1999, bool targetFloatingPo if (parts.Length < 10 || !useRelaxedValidation && parts.Length != 10 && parts.Length != 13) throw new InvalidOperationException($"Unexpected number of line image elements for analog channel definition: {parts.Length} - expected 10 or 13{Environment.NewLine}Image = {lineImage}"); - Index = int.Parse(parts[0].Trim()); + Index = int.Parse(parts[0].Trim(), CultureInfo.InvariantCulture); Name = parts[1]; Units = parts[4]; // Assign Units before PhaseID PhaseID = parts[2]; CircuitComponent = parts[3]; - Multiplier = double.Parse(parts[5].Trim()); - Adder = double.Parse(parts[6].Trim()); - Skew = double.Parse(parts[7].Trim()); - MinValue = double.Parse(parts[8].Trim()); - MaxValue = double.Parse(parts[9].Trim()); + Multiplier = double.Parse(parts[5].Trim(), CultureInfo.InvariantCulture); + Adder = double.Parse(parts[6].Trim(), CultureInfo.InvariantCulture); + Skew = double.Parse(parts[7].Trim(), CultureInfo.InvariantCulture); + MinValue = double.Parse(parts[8].Trim(), CultureInfo.InvariantCulture); + MaxValue = double.Parse(parts[9].Trim(), CultureInfo.InvariantCulture); if (parts.Length >= 13) { - PrimaryRatio = double.Parse(parts[10].Trim()); - SecondaryRatio = double.Parse(parts[11].Trim()); + PrimaryRatio = double.Parse(parts[10].Trim(), CultureInfo.InvariantCulture); + SecondaryRatio = double.Parse(parts[11].Trim(), CultureInfo.InvariantCulture); ScalingIdentifier = parts[12].Trim()[0]; } } @@ -561,7 +561,7 @@ public override string ToString() // An,ch_id,ph,ccbm,uu,a,b,skew,min,max List values = new() { - Index.ToString(), + Index.ToString(CultureInfo.InvariantCulture), Name ?? string.Empty, PhaseID, CircuitComponent ?? string.Empty, diff --git a/src/Gemstone.COMTRADE/DigitalChannel.cs b/src/Gemstone.COMTRADE/DigitalChannel.cs index c6c1618..04f1586 100644 --- a/src/Gemstone.COMTRADE/DigitalChannel.cs +++ b/src/Gemstone.COMTRADE/DigitalChannel.cs @@ -26,6 +26,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using Gemstone.StringExtensions; #if NETSTANDARD using Newtonsoft.Json; @@ -79,7 +80,7 @@ public DigitalChannel(string lineImage, int version = 1999, bool useRelaxedValid if (parts.Length >= 5) { - Index = int.Parse(parts[0].Trim()); + Index = int.Parse(parts[0].Trim(), CultureInfo.InvariantCulture); Name = parts[1]; PhaseID = parts[2]; CircuitComponent = parts[3]; @@ -87,7 +88,7 @@ public DigitalChannel(string lineImage, int version = 1999, bool useRelaxedValid } else { - Index = int.Parse(parts[0].Trim()); + Index = int.Parse(parts[0].Trim(), CultureInfo.InvariantCulture); Name = parts[1]; NormalState = parts[2].Trim().ParseBoolean(); } @@ -203,7 +204,7 @@ public override string ToString() // Dn,ch_id,ph,ccbm,y values = new List { - Index.ToString(), + Index.ToString(CultureInfo.InvariantCulture), Name ?? string.Empty, PhaseID ?? string.Empty, CircuitComponent ?? string.Empty, @@ -215,7 +216,7 @@ public override string ToString() // Dn,ch_id,y values = new List { - Index.ToString(), + Index.ToString(CultureInfo.InvariantCulture), Name ?? string.Empty, NormalState ? "1" : "0" }; diff --git a/src/Gemstone.COMTRADE/Parser.cs b/src/Gemstone.COMTRADE/Parser.cs index 628477c..7795a82 100644 --- a/src/Gemstone.COMTRADE/Parser.cs +++ b/src/Gemstone.COMTRADE/Parser.cs @@ -24,6 +24,7 @@ //****************************************************************************************************** using System; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -402,7 +403,7 @@ private bool ReadNextAscii() } // Parse row of data - uint sample = uint.Parse(elems[0]); + uint sample = uint.Parse(elems[0], CultureInfo.InvariantCulture); // Capture initial sample index - this handles cases where sample index does not start at zero if (m_initialSample == uint.MaxValue) @@ -426,7 +427,7 @@ private bool ReadNextAscii() // Fall back on specified microsecond time if (Timestamp == DateTime.MinValue) - Timestamp = new DateTime(Ticks.FromMicroseconds(double.Parse(elems[1]) * m_schema!.TimeFactor) + m_schema.StartTime.Value); + Timestamp = new DateTime(Ticks.FromMicroseconds(double.Parse(elems[1], CultureInfo.InvariantCulture) * m_schema!.TimeFactor) + m_schema.StartTime.Value); // Apply timestamp offset to restore UTC timezone if (AdjustToUTC) @@ -438,7 +439,7 @@ private bool ReadNextAscii() // Parse all record values for (int i = 0; i < Values.Length; i++) { - Values[i] = double.Parse(elems[i + 2]); + Values[i] = double.Parse(elems[i + 2], CultureInfo.InvariantCulture); if (i < m_schema!.AnalogChannels?.Length) Values[i] = AdjustValue(Values[i], i); diff --git a/src/Gemstone.COMTRADE/SampleRate.cs b/src/Gemstone.COMTRADE/SampleRate.cs index 9e4a209..6372746 100644 --- a/src/Gemstone.COMTRADE/SampleRate.cs +++ b/src/Gemstone.COMTRADE/SampleRate.cs @@ -64,8 +64,8 @@ public SampleRate(string lineImage, bool useRelaxedValidation = false) if (parts.Length < 2 || (!useRelaxedValidation && parts.Length != 2)) throw new InvalidOperationException($"Unexpected number of line image elements for sample rate definition: {parts.Length} - expected 2{Environment.NewLine}Image = {lineImage}"); - Rate = double.Parse(parts[0].Trim()); - EndSample = long.Parse(parts[1].Trim()); + Rate = double.Parse(parts[0].Trim(), CultureInfo.InvariantCulture); + EndSample = long.Parse(parts[1].Trim(), CultureInfo.InvariantCulture); } #endregion @@ -76,7 +76,15 @@ public SampleRate(string lineImage, bool useRelaxedValidation = false) /// Converts to its string format. /// public override string ToString() => // samp,endsamp - $"{Rate.ToString(CultureInfo.InvariantCulture)},{EndSample}"; + Format($"{Rate},{EndSample}"); + + #endregion + + #region [ Static ] + + // Static Methods + private static string Format(FormattableString formattableString) => + formattableString.ToString(CultureInfo.InvariantCulture); #endregion } diff --git a/src/Gemstone.COMTRADE/Schema.cs b/src/Gemstone.COMTRADE/Schema.cs index 862d5e9..5cb455b 100644 --- a/src/Gemstone.COMTRADE/Schema.cs +++ b/src/Gemstone.COMTRADE/Schema.cs @@ -248,7 +248,7 @@ public Schema(string fileName, bool useRelaxedValidation) DeviceID = parts[1].Trim(); if (parts.Length >= 3 && !string.IsNullOrWhiteSpace(parts[2])) - Version = int.Parse(parts[2].Trim()); + Version = int.Parse(parts[2].Trim(), CultureInfo.InvariantCulture); else Version = 1991; @@ -258,9 +258,9 @@ public Schema(string fileName, bool useRelaxedValidation) if (parts.Length < 3 || (!useRelaxedValidation && parts.Length != 3)) throw new InvalidOperationException($"Unexpected number of line image elements for second configuration file line: {parts.Length} - expected 3{Environment.NewLine}Image = {lines[lineNumber - 1]}"); - int totalChannels = int.Parse(parts[0].Trim()); - int totalAnalogChannels = int.Parse(parts[1].Trim().Split('A')[0]); - int totalDigitalChannels = int.Parse(parts[2].Trim().Split('D')[0]); + int totalChannels = int.Parse(parts[0].Trim(), CultureInfo.InvariantCulture); + int totalAnalogChannels = int.Parse(parts[1].Trim().Split('A')[0], CultureInfo.InvariantCulture); + int totalDigitalChannels = int.Parse(parts[2].Trim().Split('D')[0], CultureInfo.InvariantCulture); if (totalChannels != totalAnalogChannels + totalDigitalChannels) throw new InvalidOperationException($"Total defined channels must equal the sum of the total number of analog and digital channel definitions.{Environment.NewLine}Image = {lines[lineNumber - 1]}"); @@ -280,10 +280,10 @@ public Schema(string fileName, bool useRelaxedValidation) DigitalChannels = digitalChannels.ToArray(); // Parse line frequency - NominalFrequency = double.Parse(lines[lineNumber++]); + NominalFrequency = double.Parse(lines[lineNumber++], CultureInfo.InvariantCulture); // Parse total number of sample rates - int totalSampleRates = int.Parse(lines[lineNumber++]); + int totalSampleRates = int.Parse(lines[lineNumber++], CultureInfo.InvariantCulture); if (totalSampleRates == 0) totalSampleRates = 1; @@ -310,7 +310,7 @@ public Schema(string fileName, bool useRelaxedValidation) AnalogChannels = analogLineImages.Select(lineImage => new AnalogChannel(lineImage, Version, targetFloatingPoint, useRelaxedValidation)).ToArray(); // Parse time factor - TimeFactor = lineNumber < lines.Length ? double.Parse(lines[lineNumber++]) : 1; + TimeFactor = lineNumber < lines.Length ? double.Parse(lines[lineNumber++], CultureInfo.InvariantCulture) : 1; // Parse time information line if (lineNumber < lines.Length) @@ -339,10 +339,10 @@ public Schema(string fileName, bool useRelaxedValidation) parts = lines[lineNumber/*++*/].Split(','); if (parts.Length > 0) - TimeQualityIndicatorCode = (TimeQualityIndicatorCode)byte.Parse(parts[0], NumberStyles.HexNumber); + TimeQualityIndicatorCode = (TimeQualityIndicatorCode)byte.Parse(parts[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture); if (parts.Length > 1) - LeapSecondIndicator = (LeapSecondIndicator)byte.Parse(parts[1]); + LeapSecondIndicator = (LeapSecondIndicator)byte.Parse(parts[1], CultureInfo.InvariantCulture); } } @@ -523,11 +523,11 @@ public string FileImage { StringBuilder fileImage = new(); - void appendLine(string line) + void appendLine(FormattableString line) { // The standard .NET "Environment.NewLine" constant can just be a line feed on some operating systems, // but the COMTRADE standard requires that end of line markers be both a carriage return and line feed. - fileImage.Append(line); + fileImage.Append(line.ToString(CultureInfo.InvariantCulture)); fileImage.Append(Writer.CRLF); } @@ -539,17 +539,17 @@ void appendLine(string line) // Write analog definitions for (int i = 0; i < TotalAnalogChannels; i++) - appendLine(AnalogChannels![i].ToString()); + appendLine($"{AnalogChannels![i]}"); // Write digital definitions for (int i = 0; i < TotalDigitalChannels; i++) - appendLine(DigitalChannels![i].ToString()); + appendLine($"{DigitalChannels![i]}"); // Write line frequency - appendLine(NominalFrequency.ToString(CultureInfo.InvariantCulture)); + appendLine($"{NominalFrequency}"); // Write total number of sample rates (zero signifies no fixed sample rates) - appendLine(TotalSampleRates.ToString()); + appendLine($"{TotalSampleRates}"); int totalSampleRates = TotalSampleRates; @@ -558,18 +558,18 @@ void appendLine(string line) // Write sample rates for (int i = 0; i < totalSampleRates; i++) - appendLine(SampleRates![i].ToString()); + appendLine($"{SampleRates![i]}"); // Write timestamps - appendLine(StartTime.ToString()); - appendLine(TriggerTime.ToString()); + appendLine($"{StartTime}"); + appendLine($"{TriggerTime}"); // Write file type - appendLine(FileType.ToString().ToUpper()); + appendLine($"{FileType.ToString().ToUpper()}"); // Write time factor if (Version >= 1999) - appendLine(TimeFactor.ToString(CultureInfo.InvariantCulture)); + appendLine($"{TimeFactor}"); // Write per data set time info and state if (Version >= 2013) diff --git a/src/Gemstone.COMTRADE/TimeOffset.cs b/src/Gemstone.COMTRADE/TimeOffset.cs index 6e00914..5268068 100644 --- a/src/Gemstone.COMTRADE/TimeOffset.cs +++ b/src/Gemstone.COMTRADE/TimeOffset.cs @@ -23,6 +23,8 @@ using System; +using System.Globalization; + #if NETSTANDARD using Newtonsoft.Json; #else @@ -76,14 +78,14 @@ public TimeOffset(string lineImage) switch (parts.Length) { case 1: - validFormat = int.TryParse(lineImage, out hours); + validFormat = int.TryParse(lineImage, NumberStyles.Integer, CultureInfo.InvariantCulture, out hours); break; case 2: { - validFormat = int.TryParse(parts[0], out hours); + validFormat = int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out hours); if (validFormat) - validFormat = int.TryParse(parts[1], out minutes); + validFormat = int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out minutes); break; } default: @@ -167,7 +169,15 @@ public long TickOffset /// Converts to its string format. /// public override string ToString() => - NotApplicable ? "x" : $"{Hours}h{Minutes:00}"; + NotApplicable ? "x" : Format($"{Hours}h{Minutes:00}"); + + #endregion + + #region [ Static ] + + // Static Methods + private static string Format(FormattableString formattableString) => + formattableString.ToString(CultureInfo.InvariantCulture); #endregion } diff --git a/src/Gemstone.COMTRADE/Timestamp.cs b/src/Gemstone.COMTRADE/Timestamp.cs index 3f9bbc9..55ee08b 100644 --- a/src/Gemstone.COMTRADE/Timestamp.cs +++ b/src/Gemstone.COMTRADE/Timestamp.cs @@ -59,11 +59,11 @@ public Timestamp(string lineImage) if (parts.Length == 4) { - double.TryParse(parts[^1], out milliseconds); + TryParse(parts[^1], out milliseconds); parts = new[] { parts[0], parts[1], parts[2] }; } - double.TryParse(parts[^1], out double seconds); + TryParse(parts[^1], out double seconds); seconds += milliseconds; @@ -97,5 +97,16 @@ public override string ToString() => Value.ToString("dd/MM/yyyy,HH:mm:ss.ffffff", CultureInfo.InvariantCulture); #endregion + + #region [ Static ] + + // Static Methods + private static bool TryParse(string s, out double result) + { + NumberStyles style = NumberStyles.Float | NumberStyles.AllowThousands; + return double.TryParse(s, style, CultureInfo.InvariantCulture, out result); + } + + #endregion } } diff --git a/src/Gemstone.COMTRADE/Writer.cs b/src/Gemstone.COMTRADE/Writer.cs index 86b0c24..7eed4d2 100644 --- a/src/Gemstone.COMTRADE/Writer.cs +++ b/src/Gemstone.COMTRADE/Writer.cs @@ -46,7 +46,7 @@ public static class Writer /// public const long MaxFileSize = 281474976710656L; - private static readonly string s_maxByteCountString = new('0', $"{MaxFileSize}".Length); + private static readonly string s_maxByteCountString = new('0', MaxFileSize.ToString(CultureInfo.InvariantCulture).Length); /// /// Defines the maximum COMTRADE end sample number. @@ -527,8 +527,8 @@ string readLine() if (parts.Length < 3) throw new InvalidOperationException($"Unexpected number of line image elements for second configuration file line: {parts.Length} - expected 3{Environment.NewLine}Image = {line}"); - int totalAnalogChannels = int.Parse(parts[1].Trim().Split('A')[0]); - int totalDigitalChannels = int.Parse(parts[2].Trim().Split('D')[0]); + int totalAnalogChannels = int.Parse(parts[1].Trim().Split('A')[0], CultureInfo.InvariantCulture); + int totalDigitalChannels = int.Parse(parts[2].Trim().Split('D')[0], CultureInfo.InvariantCulture); // Skip analog definitions for (int i = 0; i < totalAnalogChannels; i++) @@ -542,7 +542,7 @@ string readLine() readLine(); // Parse total number of sample rates - int totalSampleRates = int.Parse(readLine()); + int totalSampleRates = int.Parse(readLine(), CultureInfo.InvariantCulture); if (totalSampleRates == 0) totalSampleRates = 1; @@ -629,9 +629,10 @@ public static void WriteNextRecordAscii(StreamWriter output, Schema schema, Tick StringBuilder line = new(); bool isFirstDigital = true; - line.Append(sample); - line.Append(','); - line.Append(microseconds); + void append(FormattableString formattableString) => + line.Append(formattableString.ToString(CultureInfo.InvariantCulture)); + + append($"{sample},{microseconds}"); for (int i = 0; i < values.Length; i++) { @@ -642,8 +643,7 @@ public static void WriteNextRecordAscii(StreamWriter output, Schema schema, Tick value -= schema.AnalogChannels[i].Adder; value /= schema.AnalogChannels[i].Multiplier; - line.Append(','); - line.Append(value.ToString(CultureInfo.InvariantCulture)); + append($",{value}"); } else { @@ -656,8 +656,8 @@ public static void WriteNextRecordAscii(StreamWriter output, Schema schema, Tick { for (int j = 0; j < 16; j++) { - line.Append(','); - line.Append(fracSecValue.CheckBits(BitOps.BitVal(j)) ? 1 : 0); + int fracSecBit = fracSecValue.CheckBits(BitOps.BitVal(j)) ? 1 : 0; + append($",{fracSecBit}"); } } } @@ -666,8 +666,8 @@ public static void WriteNextRecordAscii(StreamWriter output, Schema schema, Tick for (int j = 0; j < 16; j++) { - line.Append(','); - line.Append(digitalWord.CheckBits(BitOps.BitVal(j)) ? 1 : 0); + int digitalValue = digitalWord.CheckBits(BitOps.BitVal(j)) ? 1 : 0; + append($",{digitalValue}"); } } } @@ -677,8 +677,8 @@ public static void WriteNextRecordAscii(StreamWriter output, Schema schema, Tick { for (int j = 0; j < 16; j++) { - line.Append(','); - line.Append(fracSecValue.CheckBits(BitOps.BitVal(j)) ? 1 : 0); + int fracSecBit = fracSecValue.CheckBits(BitOps.BitVal(j)) ? 1 : 0; + append($",{fracSecBit}"); } }