using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Numerics; using System.Text; using InfluxDB.Client.Api.Domain; using InfluxDB.Client.Core; using NodaTime; namespace InfluxDB.Client.Writes { /// /// Point defines the values that will be written to the database. /// See Go Implementation. /// public partial class PointData : IEquatable { private static readonly DateTime EpochStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private readonly string _measurementName; private readonly ImmutableSortedDictionary _tags = ImmutableSortedDictionary .Empty; private readonly ImmutableSortedDictionary _fields = ImmutableSortedDictionary.Empty; public readonly WritePrecision Precision; private readonly BigInteger? _time; private PointData(string measurementName) { Arguments.CheckNonEmptyString(measurementName, "Measurement name"); _measurementName = measurementName; Precision = WritePrecision.Ns; } /// /// Create a new Point withe specified a measurement name. /// /// the measurement name /// the new Point public static PointData Measurement(string measurementName) { return new PointData(measurementName); } private PointData(string measurementName, WritePrecision precision, BigInteger? time, ImmutableSortedDictionary tags, ImmutableSortedDictionary fields) { _measurementName = measurementName; Precision = precision; _time = time; _tags = tags; _fields = fields; } /// /// Adds or replaces a tag value for a point. /// /// the tag name /// the tag value /// this public PointData Tag(string name, string value) { var isEmptyValue = string.IsNullOrEmpty(value); var tags = _tags; if (isEmptyValue) { if (tags.ContainsKey(name)) { Trace.TraceWarning( $"Empty tags will cause deletion of, tag [{name}], measurement [{_measurementName}]"); } else { Trace.TraceWarning($"Empty tags has no effect, tag [{name}], measurement [{_measurementName}]"); return this; } } if (tags.ContainsKey(name)) { tags = tags.Remove(name); } if (!isEmptyValue) { tags = tags.Add(name, value); } return new PointData(_measurementName, Precision, _time, tags, _fields); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, byte value) { return PutField(name, value); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, float value) { return PutField(name, value); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, double value) { return PutField(name, value); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, decimal value) { return PutField(name, value); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, long value) { return PutField(name, value); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, ulong value) { return PutField(name, value); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, uint value) { return PutField(name, value); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, string value) { return PutField(name, value); } /// /// Add a field with a value. /// /// the field name /// the field value /// this public PointData Field(string name, bool value) { return PutField(name, value); } /// /// Add a field with an value. /// /// the field name /// the field value /// this public PointData Field(string name, object value) { return PutField(name, value); } /// /// Updates the timestamp for the point. /// /// the timestamp /// the timestamp precision /// public PointData Timestamp(long timestamp, WritePrecision timeUnit) { return new PointData(_measurementName, timeUnit, timestamp, _tags, _fields); } /// /// Updates the timestamp for the point represented by . /// /// the timestamp /// the timestamp precision /// public PointData Timestamp(TimeSpan timestamp, WritePrecision timeUnit) { var time = TimeSpanToBigInteger(timestamp, timeUnit); return new PointData(_measurementName, timeUnit, time, _tags, _fields); } /// /// Updates the timestamp for the point represented by . /// /// the timestamp /// the timestamp precision /// public PointData Timestamp(DateTime timestamp, WritePrecision timeUnit) { var utcTimestamp = timestamp.Kind switch { DateTimeKind.Local => timestamp.ToUniversalTime(), DateTimeKind.Unspecified => DateTime.SpecifyKind(timestamp, DateTimeKind.Utc), var _ => timestamp }; var timeSpan = utcTimestamp.Subtract(EpochStart); return Timestamp(timeSpan, timeUnit); } /// /// Updates the timestamp for the point represented by . /// /// the timestamp /// the timestamp precision /// public PointData Timestamp(DateTimeOffset timestamp, WritePrecision timeUnit) { return Timestamp(timestamp.UtcDateTime, timeUnit); } /// /// Updates the timestamp for the point represented by . /// /// the timestamp /// the timestamp precision /// public PointData Timestamp(Instant timestamp, WritePrecision timeUnit) { var time = InstantToBigInteger(timestamp, timeUnit); return new PointData(_measurementName, timeUnit, time, _tags, _fields); } /// /// Has point any fields? /// /// true, if the point contains any fields, false otherwise. public bool HasFields() { return _fields.Count > 0; } /// /// The Line Protocol /// /// with the default values /// public string ToLineProtocol(PointSettings pointSettings = null) { var sb = new StringBuilder(); EscapeKey(sb, _measurementName, false); AppendTags(sb, pointSettings); var appendedFields = AppendFields(sb); if (!appendedFields) { return ""; } AppendTime(sb); return sb.ToString(); } private PointData PutField(string name, object value) { Arguments.CheckNonEmptyString(name, "Field name"); var fields = _fields; if (fields.ContainsKey(name)) { fields = fields.Remove(name); } fields = fields.Add(name, value); return new PointData(_measurementName, Precision, _time, _tags, fields); } private static BigInteger TimeSpanToBigInteger(TimeSpan timestamp, WritePrecision timeUnit) { BigInteger time; switch (timeUnit) { case WritePrecision.Ns: time = timestamp.Ticks * 100; break; case WritePrecision.Us: time = (BigInteger)(timestamp.Ticks * 0.1); break; case WritePrecision.Ms: time = (BigInteger)timestamp.TotalMilliseconds; break; case WritePrecision.S: time = (BigInteger)timestamp.TotalSeconds; break; default: throw new ArgumentOutOfRangeException(nameof(timeUnit), timeUnit, "WritePrecision value is not supported"); } return time; } private static BigInteger InstantToBigInteger(Instant timestamp, WritePrecision timeUnit) { BigInteger time; switch (timeUnit) { case WritePrecision.S: time = timestamp.ToUnixTimeSeconds(); break; case WritePrecision.Ms: time = timestamp.ToUnixTimeMilliseconds(); break; case WritePrecision.Us: time = (long)(timestamp.ToUnixTimeTicks() * 0.1); break; case WritePrecision.Ns: time = (timestamp - NodaConstants.UnixEpoch).ToBigIntegerNanoseconds(); break; default: throw new ArgumentOutOfRangeException(nameof(timeUnit), timeUnit, "WritePrecision value is not supported"); } return time; } /// /// Appends the tags. /// /// The writer. /// The point settings. private void AppendTags(StringBuilder writer, PointSettings pointSettings) { IReadOnlyDictionary entries; if (pointSettings == null) { entries = _tags; } else { var defaultTags = pointSettings.GetDefaultTags(); try { entries = _tags.AddRange(defaultTags); } catch (ArgumentException) { // Most cases don't expect to override existing content // override don't consider as best practice // therefore it a trade-off between being less efficient // on the default behavior or on the override scenario var builder = _tags.ToBuilder(); foreach (var item in defaultTags) { var name = item.Key; if (!builder.ContainsKey(name)) // existing tags overrides { builder.Add(name, item.Value); } } entries = builder; } } foreach (var keyValue in entries) { var key = keyValue.Key; var value = keyValue.Value; if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) { continue; } writer.Append(','); EscapeKey(writer, key); writer.Append('='); EscapeKey(writer, value); } writer.Append(' '); } /// /// Appends the fields. /// /// The sb. /// private bool AppendFields(StringBuilder sb) { var appended = false; foreach (var keyValue in _fields) { var key = keyValue.Key; var value = keyValue.Value; if (IsNotDefined(value)) { continue; } EscapeKey(sb, key); sb.Append('='); if (value is double || value is float) { sb.Append(((IConvertible)value).ToString(CultureInfo.InvariantCulture)); } else if (value is uint || value is ulong || value is ushort) { sb.Append(((IConvertible)value).ToString(CultureInfo.InvariantCulture)); sb.Append('u'); } else if (value is byte || value is int || value is long || value is sbyte || value is short) { sb.Append(((IConvertible)value).ToString(CultureInfo.InvariantCulture)); sb.Append('i'); } else if (value is bool b) { sb.Append(b ? "true" : "false"); } else if (value is string s) { sb.Append('"'); EscapeValue(sb, s); sb.Append('"'); } else if (value is IConvertible c) { sb.Append(c.ToString(CultureInfo.InvariantCulture)); } else { sb.Append('"'); EscapeValue(sb, value.ToString()); sb.Append('"'); } sb.Append(','); appended = true; } if (appended) { sb.Remove(sb.Length - 1, 1); } return appended; } /// /// Appends the time. /// /// The sb. private void AppendTime(StringBuilder sb) { if (_time == null) { return; } sb.Append(' '); sb.Append(((BigInteger)_time).ToString(CultureInfo.InvariantCulture)); } /// /// Escapes the key. /// /// The sb. /// The key. /// Configure to escaping equal. private void EscapeKey(StringBuilder sb, string key, bool escapeEqual = true) { foreach (var c in key) { switch (c) { case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; case ' ': case ',': sb.Append("\\"); break; case '=': if (escapeEqual) { sb.Append("\\"); } break; } sb.Append(c); } } /// /// Escapes the value. /// /// The sb. /// The value. private void EscapeValue(StringBuilder sb, string value) { foreach (var c in value) { switch (c) { case '\\': case '\"': sb.Append("\\"); break; } sb.Append(c); } } /// /// Determines whether [is not defined] [the specified value]. /// /// The value. /// /// true if [is not defined] [the specified value]; otherwise, false. /// private bool IsNotDefined(object value) { return value == null || value is double d && (double.IsInfinity(d) || double.IsNaN(d)) || value is float f && (float.IsInfinity(f) || float.IsNaN(f)); } /// /// Determines whether the specified , is equal to this instance. /// /// The to compare with this instance. /// /// true if the specified is equal to this instance; otherwise, false. /// public override bool Equals(object obj) { return Equals(obj as PointData); } /// /// Indicates whether the current object is equal to another object of the same type. /// /// An object to compare with this object. /// /// true if the current object is equal to the other parameter; otherwise, false. /// public bool Equals(PointData other) { if (other == null) { return false; } var otherTags = other._tags; var result = _tags.Count == otherTags.Count && _tags.All(pair => { var key = pair.Key; var value = pair.Value; return otherTags.ContainsKey(key) && otherTags[key] == value; }); var otherFields = other._fields; result = result && _fields.Count == otherFields.Count && _fields.All(pair => { var key = pair.Key; var value = pair.Value; return otherFields.ContainsKey(key) && Equals(otherFields[key], value); }); result = result && _measurementName == other._measurementName && Precision == other.Precision && EqualityComparer.Default.Equals(_time, other._time); return result; } /// /// Returns a hash code for this instance. /// /// /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. /// public override int GetHashCode() { var hashCode = 318335609; hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_measurementName); hashCode = hashCode * -1521134295 + Precision.GetHashCode(); hashCode = hashCode * -1521134295 + _time.GetHashCode(); foreach (var pair in _tags) { hashCode = hashCode * -1521134295 + pair.Key?.GetHashCode() ?? 0; hashCode = hashCode * -1521134295 + pair.Value?.GetHashCode() ?? 0; } foreach (var pair in _fields) { hashCode = hashCode * -1521134295 + pair.Key?.GetHashCode() ?? 0; hashCode = hashCode * -1521134295 + pair.Value?.GetHashCode() ?? 0; } return hashCode; } /// /// Implements the operator ==. /// /// The left. /// The right. /// /// The result of the operator. /// public static bool operator ==(PointData left, PointData right) { return EqualityComparer.Default.Equals(left, right); } /// /// Implements the operator !=. /// /// The left. /// The right. /// /// The result of the operator. /// public static bool operator !=(PointData left, PointData right) { return !(left == right); } } }