553 lines
25 KiB
C#
553 lines
25 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Worker.Conversion;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
|
|
|
/// <summary>Maps MXAccess COM events to protobuf MxEvent messages.</summary>
|
|
public sealed class MxAccessEventMapper
|
|
{
|
|
/// <summary>
|
|
/// Per-culture cache of the explicit timestamp formats
|
|
/// <see cref="TryParseSourceTimestamp"/> tries before the general parse
|
|
/// chain. Keyed by <see cref="CultureInfo"/> because the format set is
|
|
/// derived from the culture's own patterns and the worker host's culture
|
|
/// is fixed in practice, so the derivation runs once. The cache assumes
|
|
/// <see cref="DateTimeFormatInfo"/> patterns are not customized or
|
|
/// mutated at runtime — <see cref="CultureInfo"/> compares by name, so a
|
|
/// culture carrying custom patterns is served the formats derived for its
|
|
/// stock namesake. That direction is safe: a stale format simply stops
|
|
/// matching, and the unchanged general chain below still parses the
|
|
/// string.
|
|
/// </summary>
|
|
private static readonly ConcurrentDictionary<CultureInfo, string[]> TimestampFormatCache = new();
|
|
|
|
/// <summary>Cached factory delegate so the dictionary lookup allocates nothing on the event path.</summary>
|
|
private static readonly Func<CultureInfo, string[]> TimestampFormatFactory = BuildTimestampFormats;
|
|
|
|
private readonly VariantConverter variantConverter;
|
|
private readonly MxStatusProxyConverter statusProxyConverter;
|
|
|
|
/// <summary>Initializes a new instance of the MxAccessEventMapper class with default converters.</summary>
|
|
public MxAccessEventMapper()
|
|
: this(new VariantConverter(), new MxStatusProxyConverter())
|
|
{
|
|
}
|
|
|
|
/// <summary>Initializes a new instance of the MxAccessEventMapper class with provided converters.</summary>
|
|
/// <param name="variantConverter">Converter for MXAccess variant values to MxValue protobuf messages.</param>
|
|
/// <param name="statusProxyConverter">Converter for MXAccess status arrays to MxStatusProxy protobuf messages.</param>
|
|
public MxAccessEventMapper(
|
|
VariantConverter variantConverter,
|
|
MxStatusProxyConverter statusProxyConverter)
|
|
{
|
|
this.variantConverter = variantConverter ?? throw new ArgumentNullException(nameof(variantConverter));
|
|
this.statusProxyConverter = statusProxyConverter ?? throw new ArgumentNullException(nameof(statusProxyConverter));
|
|
}
|
|
|
|
/// <summary>Creates an OnDataChange event from MXAccess COM event arguments.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="value">Item value received from MXAccess.</param>
|
|
/// <param name="quality">Item quality code from MXAccess.</param>
|
|
/// <param name="timestamp">Item timestamp from MXAccess.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <returns>The mapped OnDataChange event.</returns>
|
|
public MxEvent CreateOnDataChange(
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
object? value,
|
|
int quality,
|
|
object? timestamp,
|
|
Array? statuses)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnDataChange,
|
|
sessionId,
|
|
serverHandle,
|
|
itemHandle,
|
|
statuses);
|
|
mxEvent.Value = variantConverter.Convert(value);
|
|
mxEvent.Quality = quality;
|
|
ApplySourceTimestamp(mxEvent, timestamp);
|
|
mxEvent.OnDataChange = new OnDataChangeEvent();
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>Creates an OnWriteComplete event from MXAccess COM event arguments.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <returns>The mapped OnWriteComplete event.</returns>
|
|
public MxEvent CreateOnWriteComplete(
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
Array? statuses)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnWriteComplete,
|
|
sessionId,
|
|
serverHandle,
|
|
itemHandle,
|
|
statuses);
|
|
mxEvent.OnWriteComplete = new OnWriteCompleteEvent();
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>Creates an OperationComplete event from MXAccess COM event arguments.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <returns>The mapped OperationComplete event.</returns>
|
|
public MxEvent CreateOperationComplete(
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
Array? statuses)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OperationComplete,
|
|
sessionId,
|
|
serverHandle,
|
|
itemHandle,
|
|
statuses);
|
|
mxEvent.OperationComplete = new OperationCompleteEvent();
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an OnAlarmTransition event from MXAccess alarm-event arguments.
|
|
/// The worker's alarm path drives this method from
|
|
/// <see cref="MxAccessAlarmEventSink.EnqueueTransition"/> once
|
|
/// <see cref="AlarmDispatcher"/> decodes a transition raised by the
|
|
/// wnwrap-backed <see cref="WnWrapAlarmConsumer"/>.
|
|
/// </summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="alarmFullReference">Fully-qualified MxAccess alarm reference (e.g. "Tank01.Level.HiHi").</param>
|
|
/// <param name="sourceObjectReference">Galaxy-side source object reference; empty when not bound to a Galaxy object.</param>
|
|
/// <param name="alarmTypeName">MxAccess alarm-type qualifier (e.g. "AnalogLimitAlarm.HiHi").</param>
|
|
/// <param name="transitionKind">Discriminator: Raise / Acknowledge / Clear / Retrigger.</param>
|
|
/// <param name="severity">Raw MxAccess severity (kept on the native scale; lmxopcua maps to OPC UA 0-1000).</param>
|
|
/// <param name="originalRaiseTimestampUtc">When the alarm originally entered active; null on retrigger.</param>
|
|
/// <param name="transitionTimestampUtc">When this specific transition occurred.</param>
|
|
/// <param name="operatorUser">Operator principal recorded by MxAccess on Acknowledge transitions; empty on raise/clear.</param>
|
|
/// <param name="operatorComment">Operator-supplied comment recorded by MxAccess on Acknowledge transitions; empty on raise/clear.</param>
|
|
/// <param name="category">Alarm taxonomy bucket from the Galaxy template.</param>
|
|
/// <param name="description">Human-readable alarm description.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <param name="degraded">
|
|
/// <see langword="true"/> when this transition was synthesized by the
|
|
/// subtag-provider fallback rather than the native alarmmgr path.
|
|
/// Defaults to <see langword="false"/> to preserve alarmmgr parity.
|
|
/// </param>
|
|
/// <param name="sourceProvider">
|
|
/// The alarm provider that sourced this transition. Defaults to
|
|
/// <see cref="AlarmProviderMode.Alarmmgr"/> for the native path.
|
|
/// </param>
|
|
/// <returns>The mapped OnAlarmTransition event.</returns>
|
|
public MxEvent CreateOnAlarmTransition(
|
|
string sessionId,
|
|
string alarmFullReference,
|
|
string sourceObjectReference,
|
|
string alarmTypeName,
|
|
AlarmTransitionKind transitionKind,
|
|
int severity,
|
|
DateTime? originalRaiseTimestampUtc,
|
|
DateTime transitionTimestampUtc,
|
|
string operatorUser,
|
|
string operatorComment,
|
|
string category,
|
|
string description,
|
|
Array? statuses,
|
|
bool degraded = false,
|
|
AlarmProviderMode sourceProvider = AlarmProviderMode.Alarmmgr)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnAlarmTransition,
|
|
sessionId,
|
|
serverHandle: 0,
|
|
itemHandle: 0,
|
|
statuses);
|
|
|
|
OnAlarmTransitionEvent body = new()
|
|
{
|
|
AlarmFullReference = alarmFullReference ?? string.Empty,
|
|
SourceObjectReference = sourceObjectReference ?? string.Empty,
|
|
AlarmTypeName = alarmTypeName ?? string.Empty,
|
|
TransitionKind = transitionKind,
|
|
Severity = severity,
|
|
TransitionTimestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
|
|
DateTime.SpecifyKind(transitionTimestampUtc, DateTimeKind.Utc)),
|
|
OperatorUser = operatorUser ?? string.Empty,
|
|
OperatorComment = operatorComment ?? string.Empty,
|
|
Category = category ?? string.Empty,
|
|
Description = description ?? string.Empty,
|
|
Degraded = degraded,
|
|
SourceProvider = sourceProvider,
|
|
};
|
|
if (originalRaiseTimestampUtc is { } orts)
|
|
{
|
|
body.OriginalRaiseTimestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
|
|
DateTime.SpecifyKind(orts, DateTimeKind.Utc));
|
|
}
|
|
mxEvent.OnAlarmTransition = body;
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an OnAlarmProviderModeChanged event from a
|
|
/// <see cref="FailoverAlarmConsumer"/> provider switch. The worker's
|
|
/// alarm path drives this when the failover composite switches between
|
|
/// the alarmmgr primary and the subtag standby, so connected gateway
|
|
/// clients can observe the degraded/recovered provider state.
|
|
/// </summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="mode">The provider mode now active after the switch.</param>
|
|
/// <param name="reason">Human-readable reason for the switch.</param>
|
|
/// <param name="hresult">The COM HRESULT that triggered a failover, or 0 for a clean failback.</param>
|
|
/// <param name="atUtc">The UTC instant the switch occurred.</param>
|
|
/// <returns>The mapped OnAlarmProviderModeChanged event.</returns>
|
|
public MxEvent CreateOnAlarmProviderModeChanged(
|
|
string sessionId,
|
|
AlarmProviderMode mode,
|
|
string reason,
|
|
int hresult,
|
|
DateTime atUtc)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnAlarmProviderModeChanged,
|
|
sessionId,
|
|
serverHandle: 0,
|
|
itemHandle: 0,
|
|
statuses: null);
|
|
|
|
mxEvent.OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent
|
|
{
|
|
Mode = mode,
|
|
Reason = reason ?? string.Empty,
|
|
Hresult = hresult,
|
|
At = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
|
|
DateTime.SpecifyKind(atUtc, DateTimeKind.Utc)),
|
|
};
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>Creates an OnBufferedDataChange event from MXAccess COM event arguments.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="rawDataType">Raw MXAccess data type code for the buffered value.</param>
|
|
/// <param name="value">Item value received from MXAccess.</param>
|
|
/// <param name="quality">Array of quality values from MXAccess.</param>
|
|
/// <param name="timestamp">Array of timestamp values from MXAccess.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <returns>The mapped OnBufferedDataChange event.</returns>
|
|
public MxEvent CreateOnBufferedDataChange(
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int rawDataType,
|
|
object? value,
|
|
object? quality,
|
|
object? timestamp,
|
|
Array? statuses)
|
|
{
|
|
MxDataType dataType = MapMxDataType(rawDataType);
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnBufferedDataChange,
|
|
sessionId,
|
|
serverHandle,
|
|
itemHandle,
|
|
statuses);
|
|
mxEvent.Value = variantConverter.Convert(value, dataType);
|
|
mxEvent.OnBufferedDataChange = new OnBufferedDataChangeEvent
|
|
{
|
|
DataType = dataType,
|
|
RawDataType = rawDataType,
|
|
QualityValues = ConvertBufferedArray(quality, MxDataType.Integer),
|
|
TimestampValues = ConvertBufferedArray(timestamp, MxDataType.Time),
|
|
};
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>Maps a raw MXAccess data type code to the MxDataType enum.</summary>
|
|
/// <param name="rawDataType">Raw MXAccess data type value to map.</param>
|
|
/// <returns>The corresponding MxDataType enum value.</returns>
|
|
public static MxDataType MapMxDataType(int rawDataType)
|
|
{
|
|
return rawDataType switch
|
|
{
|
|
-1 => MxDataType.Unknown,
|
|
0 => MxDataType.NoData,
|
|
1 => MxDataType.Boolean,
|
|
2 => MxDataType.Integer,
|
|
3 => MxDataType.Float,
|
|
4 => MxDataType.Double,
|
|
5 => MxDataType.String,
|
|
6 => MxDataType.Time,
|
|
7 => MxDataType.ElapsedTime,
|
|
8 => MxDataType.ReferenceType,
|
|
9 => MxDataType.StatusType,
|
|
10 => MxDataType.Enum,
|
|
11 => MxDataType.SecurityClassificationEnum,
|
|
12 => MxDataType.DataQualityType,
|
|
13 => MxDataType.QualifiedEnum,
|
|
14 => MxDataType.QualifiedStruct,
|
|
15 => MxDataType.InternationalizedString,
|
|
16 => MxDataType.BigString,
|
|
17 => MxDataType.End,
|
|
_ => MxDataType.Unknown,
|
|
};
|
|
}
|
|
|
|
private MxEvent CreateBaseEvent(
|
|
MxEventFamily family,
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
Array? statuses)
|
|
{
|
|
MxEvent mxEvent = new()
|
|
{
|
|
Family = family,
|
|
SessionId = sessionId ?? string.Empty,
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
};
|
|
mxEvent.Statuses.Add(statusProxyConverter.ConvertMany(statuses));
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
private void ApplySourceTimestamp(
|
|
MxEvent mxEvent,
|
|
object? timestamp)
|
|
{
|
|
MxValue convertedTimestamp = variantConverter.Convert(timestamp, MxDataType.Time);
|
|
if (convertedTimestamp.KindCase == MxValue.KindOneofCase.TimestampValue)
|
|
{
|
|
mxEvent.SourceTimestamp = convertedTimestamp.TimestampValue;
|
|
return;
|
|
}
|
|
|
|
// MXAccess fires OnDataChange with pftItemTimeStamp marshaled as a
|
|
// VT_BSTR string (e.g. "3/26/2026 1:38:22.907 PM"), not a FILETIME or
|
|
// a VT_DATE — so the variant converter classifies it as a plain
|
|
// string and the timestamp would otherwise be dropped. Parse it here
|
|
// so the source timestamp still reaches MxEvent. MXAccess formats the
|
|
// string in the worker host's local time; see TryParseSourceTimestamp.
|
|
if (convertedTimestamp.KindCase == MxValue.KindOneofCase.StringValue
|
|
&& TryParseSourceTimestamp(convertedTimestamp.StringValue, out DateTime parsedUtc))
|
|
{
|
|
mxEvent.SourceTimestamp = Timestamp.FromDateTime(parsedUtc);
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(convertedTimestamp.RawDiagnostic))
|
|
{
|
|
mxEvent.RawStatus = string.IsNullOrWhiteSpace(mxEvent.RawStatus)
|
|
? convertedTimestamp.RawDiagnostic
|
|
: $"{mxEvent.RawStatus}; {convertedTimestamp.RawDiagnostic}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses an MXAccess <c>OnDataChange</c> timestamp string into a UTC
|
|
/// <see cref="DateTime"/>. MXAccess delivers the value as a culture-
|
|
/// formatted string rather than a FILETIME or VT_DATE, and formats it
|
|
/// in the worker host's <em>local</em> time (verified empirically — a
|
|
/// fast-changing tag's timestamp lands the host's UTC offset behind
|
|
/// wall-clock UTC). The parsed value is therefore taken as local time
|
|
/// and converted to UTC. Tries the worker host's culture first
|
|
/// (MXAccess formats with the host locale), then the invariant culture.
|
|
/// <para>
|
|
/// PERF: every OnDataChange carries one of these strings, and the
|
|
/// general <see cref="DateTime.TryParse(string, IFormatProvider, DateTimeStyles, out DateTime)"/>
|
|
/// has to walk the culture's whole pattern set to recognize it. The
|
|
/// shapes MXAccess actually emits are tried first with
|
|
/// <see cref="DateTime.TryParseExact(string, string[], IFormatProvider, DateTimeStyles, out DateTime)"/>.
|
|
/// The formats are <em>derived from the current culture's own</em>
|
|
/// short-date and long-time patterns (see
|
|
/// <see cref="BuildTimestampFormats"/>) rather than hard-coded to the
|
|
/// US shapes seen in captures: a hard-coded <c>M/d/yyyy</c> would
|
|
/// reorder month and day on a day-first host relative to what the
|
|
/// general parse below returns. Deriving them means any string the
|
|
/// exact parse accepts is one the culture-aware general parse would
|
|
/// have accepted with the same value, so the fast path is a shortcut
|
|
/// and never a different answer; anything it does not match falls
|
|
/// through to the unchanged two-stage chain.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="text">The MXAccess timestamp string.</param>
|
|
/// <param name="utc">The parsed UTC timestamp on success.</param>
|
|
/// <returns><see langword="true"/> when the string parsed successfully.</returns>
|
|
internal static bool TryParseSourceTimestamp(string? text, out DateTime utc)
|
|
{
|
|
utc = default;
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const DateTimeStyles styles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal;
|
|
CultureInfo culture = CultureInfo.CurrentCulture;
|
|
|
|
// Empty when the culture's patterns yielded no format that survived
|
|
// validation; that culture just gets no fast path.
|
|
string[] formats = TimestampFormatCache.GetOrAdd(culture, TimestampFormatFactory);
|
|
DateTime parsed;
|
|
if ((formats.Length > 0 && DateTime.TryParseExact(text, formats, culture, styles, out parsed))
|
|
|| DateTime.TryParse(text, culture, styles, out parsed)
|
|
|| DateTime.TryParse(text, CultureInfo.InvariantCulture, styles, out parsed))
|
|
{
|
|
utc = DateTime.SpecifyKind(parsed, DateTimeKind.Utc);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the explicit timestamp formats for a culture: the culture's
|
|
/// short date plus its long time, and — first, because it is the shape
|
|
/// MXAccess actually fires — the same pattern with the millisecond
|
|
/// fraction MXAccess appends to the seconds ("3/26/2026 1:38:22.907 PM"
|
|
/// under en-US). No culture publishes a long-time pattern with
|
|
/// milliseconds, so the fractional sibling is derived by inserting
|
|
/// <c>.fff</c> after the seconds specifier; a pattern without a
|
|
/// two-digit seconds specifier, or one that already carries a fraction,
|
|
/// gets only the published pattern.
|
|
/// <para>
|
|
/// Both candidates are validated by round-tripping a probe instant
|
|
/// before they are handed to the event path, because the derivation
|
|
/// reads patterns it does not control: the first <c>ss</c> in a
|
|
/// pattern could in principle sit inside quoted literal text, and
|
|
/// inserting into it would produce a format that either mangles the
|
|
/// value or is outright malformed — and a malformed format makes
|
|
/// <see cref="DateTime.ParseExact(string, string, IFormatProvider)"/>
|
|
/// and its siblings throw <see cref="FormatException"/> rather than
|
|
/// report failure. Validation happens here, once per culture, so the
|
|
/// event path never carries that risk; a candidate that does not
|
|
/// round-trip is dropped, and a culture that loses both simply gets
|
|
/// no fast path.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="culture">Culture whose patterns the formats are derived from.</param>
|
|
/// <returns>The validated formats to try, most likely first; empty when none survived.</returns>
|
|
private static string[] BuildTimestampFormats(CultureInfo culture)
|
|
{
|
|
DateTimeFormatInfo formatInfo = culture.DateTimeFormat;
|
|
string shortDate = formatInfo.ShortDatePattern;
|
|
string longTime = formatInfo.LongTimePattern;
|
|
List<string> formats = new(2);
|
|
|
|
int secondsIndex = longTime.IndexOf("ss", StringComparison.Ordinal);
|
|
if (secondsIndex >= 0 && longTime.IndexOf('f') < 0 && longTime.IndexOf('F') < 0)
|
|
{
|
|
string withMilliseconds = shortDate + " " + longTime.Insert(secondsIndex + 2, ".fff");
|
|
if (RoundTripsProbe(withMilliseconds, culture, new DateTime(2026, 3, 26, 13, 38, 22, 907)))
|
|
{
|
|
formats.Add(withMilliseconds);
|
|
}
|
|
}
|
|
|
|
string published = shortDate + " " + longTime;
|
|
if (RoundTripsProbe(published, culture, new DateTime(2026, 3, 26, 13, 38, 22)))
|
|
{
|
|
formats.Add(published);
|
|
}
|
|
|
|
return formats.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reports whether a derived format both formats and re-parses a probe
|
|
/// instant back to itself under the given culture. A format that throws,
|
|
/// fails to parse its own output, or loses part of the instant is
|
|
/// rejected — the fast path may only ever be a shortcut.
|
|
/// </summary>
|
|
/// <param name="format">Candidate format to validate.</param>
|
|
/// <param name="culture">Culture the format was derived from.</param>
|
|
/// <param name="probe">Instant to round-trip; carries the precision the format must preserve.</param>
|
|
/// <returns><see langword="true"/> when the format is safe to use on the event path.</returns>
|
|
private static bool RoundTripsProbe(
|
|
string format,
|
|
CultureInfo culture,
|
|
DateTime probe)
|
|
{
|
|
try
|
|
{
|
|
string probeText = probe.ToString(format, culture);
|
|
return DateTime.TryParseExact(probeText, format, culture, DateTimeStyles.None, out DateTime roundTripped)
|
|
&& roundTripped == probe;
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private MxArray ConvertBufferedArray(
|
|
object? value,
|
|
MxDataType expectedElementDataType)
|
|
{
|
|
if (value is Array array)
|
|
{
|
|
return variantConverter.ConvertArray(array, expectedElementDataType);
|
|
}
|
|
|
|
MxValue converted = variantConverter.Convert(value, expectedElementDataType);
|
|
if (converted.KindCase == MxValue.KindOneofCase.ArrayValue)
|
|
{
|
|
return converted.ArrayValue;
|
|
}
|
|
|
|
MxArray mxArray = new()
|
|
{
|
|
ElementDataType = converted.DataType,
|
|
VariantType = converted.VariantType,
|
|
RawElementDataType = converted.RawDataType,
|
|
RawDiagnostic = string.IsNullOrWhiteSpace(converted.RawDiagnostic)
|
|
? "Buffered MXAccess event argument was not a SAFEARRAY."
|
|
: converted.RawDiagnostic,
|
|
};
|
|
|
|
switch (converted.KindCase)
|
|
{
|
|
case MxValue.KindOneofCase.Int32Value:
|
|
mxArray.Int32Values = new Int32Array();
|
|
mxArray.Int32Values.Values.Add(converted.Int32Value);
|
|
break;
|
|
|
|
case MxValue.KindOneofCase.Int64Value:
|
|
mxArray.Int64Values = new Int64Array();
|
|
mxArray.Int64Values.Values.Add(converted.Int64Value);
|
|
break;
|
|
|
|
case MxValue.KindOneofCase.TimestampValue:
|
|
mxArray.TimestampValues = new TimestampArray();
|
|
mxArray.TimestampValues.Values.Add(converted.TimestampValue);
|
|
break;
|
|
}
|
|
|
|
return mxArray;
|
|
}
|
|
}
|