using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// A lightweight struct representing a time-value pair.
/// Pure data type: 16 bytes (long + double).
/// Implements ISpanFormattable for allocation-free formatting.
///
[SkipLocalsInit]
[StructLayout(LayoutKind.Auto)]
public readonly record struct TValue(long Time, double Value) : ISpanFormattable
{
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue(DateTime time, double value)
: this(time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks, value)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator double(TValue tv) => tv.Value;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator DateTime(TValue tv) => new(tv.Time, DateTimeKind.Utc);
///
/// Implicitly converts a raw double to a TValue with DateTime.UtcNow timestamp.
/// Enables ergonomic APIs like sma.Update(110.4).
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator TValue(double value) => new(DateTime.UtcNow, value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString()
{
string valueStr = Value switch
{
double.PositiveInfinity => ((char)0x221E).ToString(),
double.NegativeInfinity => "-" + (char)0x221E,
_ when double.IsNaN(Value) => "NaN",
_ => Value.ToString("F2"),
};
return $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {valueStr}]";
}
///
/// Formats the TValue using the specified format string.
/// Note: Custom format and formatProvider are not supported by TValue.
/// If a non-null/non-empty format is provided, a NotSupportedException is thrown.
///
/// Must be null or empty; custom formats are not supported.
/// Ignored; TValue uses its own fixed format.
/// The string representation of this TValue.
/// Thrown when a non-null/non-empty format is provided.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ToString(string? format, IFormatProvider? formatProvider)
{
if (!string.IsNullOrEmpty(format))
{
throw new NotSupportedException($"Custom format '{format}' is not supported by TValue. Use ToString() for the default format.");
}
return ToString();
}
///
/// Formats the TValue into the provided span without heap allocation.
/// Format: "[yyyy-MM-dd HH:mm:ss, value]"
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
{
charsWritten = 0;
// Early reject for buffers too small to hold even the timestamp portion.
// This is a heuristic check; actual buffer-overflow protection is performed
// by the explicit length checks that guard each write operation below.
if (destination.Length < 24)
{
return false;
}
// Write opening bracket
destination[0] = '[';
int pos = 1;
// Format datetime: yyyy-MM-dd HH:mm:ss (19 chars)
if (!AsDateTime.TryFormat(destination.Slice(pos), out int dtChars, "yyyy-MM-dd HH:mm:ss", provider))
{
return false;
}
pos += dtChars;
// Write separator
if (pos + 2 > destination.Length)
{
return false;
}
destination[pos++] = ',';
destination[pos++] = ' ';
// Format value
if (double.IsPositiveInfinity(Value))
{
if (pos + 1 > destination.Length)
{
return false;
}
destination[pos++] = (char)0x221E; //
}
else if (double.IsNegativeInfinity(Value))
{
if (pos + 2 > destination.Length)
{
return false;
}
destination[pos++] = '-';
destination[pos++] = (char)0x221E; // -
}
else if (double.IsNaN(Value))
{
if (pos + 3 > destination.Length)
{
return false;
}
destination[pos++] = 'N';
destination[pos++] = 'a';
destination[pos++] = 'N';
}
else
{
if (!Value.TryFormat(destination.Slice(pos), out int valueChars, "F2", provider))
{
return false;
}
pos += valueChars;
}
// Write closing bracket
if (pos + 1 > destination.Length)
{
return false;
}
destination[pos++] = ']';
charsWritten = pos;
return true;
}
}