Files
Miha Kralj f10baa6dfb docs: add license rationale, Python/PineScript guides, API updates
- Add docs/license.md with Apache 2.0 rationale and patent protection analysis
- Add docs/python.md and docs/pinescript.md platform guides
- Expand README license section with disclosure and link to rationale
- Update docs/api.md and docs/architecture.md
- Update Python bindings: helpers, all indicator modules, pyproject.toml
- Add Python tests for Arrow and Polars integration
- Update TValue core type and documentation
- Add fix_length_to_period tooling script
2026-03-03 22:11:35 -08:00

160 lines
5.1 KiB
C#

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// A lightweight struct representing a time-value pair.
/// Pure data type: 16 bytes (long + double).
/// Implements ISpanFormattable for allocation-free formatting.
/// </summary>
[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);
/// <summary>
/// Implicitly converts a raw double to a TValue with DateTime.UtcNow timestamp.
/// Enables ergonomic APIs like <c>sma.Update(110.4)</c>.
/// </summary>
[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}]";
}
/// <summary>
/// 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.
/// </summary>
/// <param name="format">Must be null or empty; custom formats are not supported.</param>
/// <param name="formatProvider">Ignored; TValue uses its own fixed format.</param>
/// <returns>The string representation of this TValue.</returns>
/// <exception cref="NotSupportedException">Thrown when a non-null/non-empty format is provided.</exception>
[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();
}
/// <summary>
/// Formats the TValue into the provided span without heap allocation.
/// Format: "[yyyy-MM-dd HH:mm:ss, value]"
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> 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;
}
}