// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; using System.Text; namespace OpenTap { class Range : IEnumerable { public readonly BigFloat Start; public readonly BigFloat Stop; public readonly BigFloat Step; public IEnumerator GetEnumerator() { var start = Start; var stop = Stop; var step = Step; if (Start == Stop) { // A a range of 0 elements will be created otherwise. So return Start. yield return Start; yield break; } // This calculation is based on linear extrapolation that includes the end point // on the condition that it's within double.epsilon var approximate_steps = ((stop - start) / step).Abs(); // Cancel the last step if it's the calculated spot is more than double.epsilon further away. BigInteger steps = approximate_steps.Rounded(); for (BigInteger i = 0; i <= steps; i++) { yield return new BigFloat(i) * step + start; } } public Range(BigFloat start, BigFloat stop) { Start = start; Stop = stop; Step = (Stop - Start).Sign(); CheckRange(); } public Range(BigFloat start, BigFloat stop, BigFloat step) { Start = start; Stop = stop; Step = step; CheckRange(); } public void CheckRange() { if (Stop == Start) return; // Math.Sign(0) == 0. if (Step == 0 || (Stop - Start).Sign() != Step.Sign()) throw new Exception("Infinite range not supported."); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public override string ToString() { return ToString(null); } public string ToString(Func conv) { conv = conv ?? ((v) => v.ToString()); if (Step == new BigFloat(1.0) || Step == new BigFloat(-1.0)) { return string.Format("{0} : {1}", conv(Start), conv(Stop)); } return string.Format("{0} : {1} : {2}", conv(Start), conv(Step), conv(Stop)); } } class UnitFormatter { static BigFloat peta = 1000000000000000L; static BigFloat tera = 1000000000000L; static BigFloat giga = 1000000000L; static BigFloat mega = 1000000; static BigFloat kilo = 1000; static BigFloat one = 1; static BigFloat mili = kilo.Invert(); static BigFloat micro = mega.Invert(); static BigFloat nano = giga.Invert(); static BigFloat pico = tera.Invert(); static BigFloat femto = peta.Invert(); static BigFloat engineeringPrefixLevel(char l) { switch (l) { case 'T': return tera; case 'G': return giga; case 'M': return mega; case 'k': return kilo; case ' ': return one; case 'm': return mili; case 'u': return micro; case 'n': return nano; case 'p': return pico; case 'f': return femto; default: throw new Exception("Invalid engineering prefix"); } } static char[] levels = { 'T', 'G', 'M', 'k', ' ', 'm', 'u', 'n', 'p', 'f' }; static char findLevel(BigFloat value) { foreach (char level in levels) { var eng = engineeringPrefixLevel(level); if (value >= eng || value <= -eng) { return level; } } return ' '; } public static void Format(StringBuilder sb, BigFloat value, bool prefix, string unit, string format, CultureInfo culture, bool compact = false) { char level = prefix ? findLevel(value) : ' '; BigFloat scaling = engineeringPrefixLevel(level); BigFloat post_scale = value / scaling; if (string.IsNullOrEmpty(format)) post_scale.AppendTo(sb, culture); else { if (format.StartsWith("x", StringComparison.OrdinalIgnoreCase)) sb.Append(((long)post_scale.Rounded()).ToString(format, culture)); else if (format.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) sb.Append("0x" + ((long)post_scale.Rounded()).ToString(format.Substring(1), culture)); else { try { sb.Append(((decimal)post_scale.ConvertTo(typeof(decimal))).ToString(format, culture)); } catch { post_scale.AppendTo(sb, culture); } } } if (level == ' ' && string.IsNullOrEmpty(unit)) { return; } string space = compact ? "" : " "; sb.Append(space); if (level != ' ') sb.Append(level); if(unit != null) sb.Append(unit); } public static string Format(BigFloat value, bool prefix, string unit, string format, CultureInfo culture, bool compact = false) { char level = prefix ? findLevel(value) : ' '; BigFloat scaling = engineeringPrefixLevel(level); BigFloat post_scale = value / scaling; string final_string; if (string.IsNullOrEmpty(format)) final_string = post_scale.ToString("R", culture); else { if (format.StartsWith("x", StringComparison.OrdinalIgnoreCase)) final_string = ((long)post_scale.Rounded()).ToString(format, culture); else if (format.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) final_string = "0x" + ((long)post_scale.Rounded()).ToString(format.Substring(1), culture); else { try { final_string = ((decimal)post_scale.ConvertTo(typeof(decimal))).ToString(format, culture); } catch { final_string = post_scale.ToString(format, culture); } } } if (level == ' ' && string.IsNullOrEmpty(unit)) { return final_string; } string space = compact ? "" : " "; if (level == ' ') return string.Format("{0}{2}{1}", final_string, unit, space); else return string.Format("{0}{3}{1}{2}", final_string, level, unit ?? "", space); } static object parse(string str, string unit, string format, CultureInfo culture) { if (str == null) return new ArgumentNullException("str"); if (unit == null) return new ArgumentNullException("unit"); if (format == null) return new ArgumentNullException("format"); if (culture == null) return new ArgumentNullException("culture"); str = str.Trim(); bool IsHex = (str.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || str.StartsWith("-0x", StringComparison.OrdinalIgnoreCase)); int HexSkip = 2; if ((!IsHex) && (format.StartsWith("x", StringComparison.OrdinalIgnoreCase))) { IsHex = true; HexSkip = 0; } if (unit.Length > 0 && str.EndsWith(unit, StringComparison.OrdinalIgnoreCase)) str = str.Remove(str.Length - unit.Length); str = str.TrimEnd(); if (str.Length == 0) { return new FormatException("Invalid format"); } char prefix = str[str.Length - 1]; BigFloat multiplier = 1.0; if (levels.Contains(prefix)) { // Handle case of "femto" unit if (!(IsHex && (prefix == 'f') && !str.EndsWith(" f", StringComparison.Ordinal))) { multiplier = engineeringPrefixLevel(prefix); if (str[str.Length - 1] == prefix) str = str.Substring(0, str.Length - 1).TrimEnd(); } } if (IsHex && str.StartsWith("-", StringComparison.OrdinalIgnoreCase)) return new BigFloat(-Int64.Parse(str.Substring(HexSkip + 1).ToUpper(), NumberStyles.HexNumber, culture)) * multiplier; else if (IsHex) return new BigFloat(BigInteger.Parse("0" + str.Substring(HexSkip).ToUpper(), NumberStyles.HexNumber, culture)) * multiplier; if (str.StartsWith("0b", StringComparison.OrdinalIgnoreCase) || str.StartsWith("-0b", StringComparison.OrdinalIgnoreCase)) { string bits = ""; if (str.StartsWith("-0b", StringComparison.OrdinalIgnoreCase)) { multiplier = -multiplier; bits = str.Substring(2); } else { bits = str.Substring(2); } BigInteger v = 0; foreach (var bit in bits) { if (bit == '1') { v = v << 1 | 1; } else if (bit == '0') { v = v << 1; } else if (char.IsWhiteSpace(bit)) continue; else { return new FormatException("Invalid binary format."); } } return new BigFloat(v) * multiplier; } var r = BigFloat.Parse(str, culture); if (r is BigFloat bf) return bf * multiplier; return r; } public static BigFloat Parse(string str, string unit, string format, CultureInfo culture) { var result = parse(str, unit, format, culture); if (result is BigFloat bf) return bf; else throw (Exception)result; } public static bool TryParse(string str, string unit, string format, CultureInfo culture, out BigFloat bf) { var result = parse(str, unit, format, culture); if (result is BigFloat _bf) { bf = _bf; return true; } bf = default(BigFloat); return false; } } /// /// A number of combined number sequences. /// public interface ICombinedNumberSequence : IEnumerable { /// /// Inner values representing the sequence. /// List> Sequences { get; } /// /// Casts the number sequence to a specific type. Should be a numeric type, e.g. typeof(float). /// /// /// ICombinedNumberSequence CastTo(Type elementType); } /// /// Generic number sequence type. /// /// public interface ICombinedNumberSequence : IEnumerable, ICombinedNumberSequence { /// /// Casts this to a new number sequence type. /// /// /// ICombinedNumberSequence CastTo(); } /// /// Extensions for ICombinedNumberSequence. /// public static class ICombinedNumberSequenceExtension { /// Casts one type ICombinedNumberSequence to another. /// /// /// public static ICombinedNumberSequence CastTo(this ICombinedNumberSequence seq) { return (ICombinedNumberSequence)seq.CastTo(typeof(T)); } } interface _ICombinedNumberSequence { List> Sequences { get; set; } } class CombinedNumberSequences : IEnumerable, ICombinedNumberSequence, _ICombinedNumberSequence { public List> Sequences { get; set; } List> ICombinedNumberSequence.Sequences { get {return Sequences.Select(x => x.Select(y => (double)y.ConvertTo(typeof(double)))).ToList(); } } public CombinedNumberSequences(List> sequences) { this.Sequences = sequences; } IEnumerable getValues() { foreach (var seq in Sequences) { foreach (BigFloat val in seq) { yield return (T)val.ConvertTo(typeof(T)); } } } public IEnumerator GetEnumerator() { return getValues().GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return getValues().GetEnumerator(); } public ICombinedNumberSequence CastTo() { return new CombinedNumberSequences(Sequences); } public ICombinedNumberSequence CastTo(Type elemType) { return (ICombinedNumberSequence)Activator.CreateInstance(typeof(CombinedNumberSequences<>).MakeGenericType(elemType), Sequences); } } /// /// Parser / back parser for numbers and sequences of numbers. /// public class NumberFormatter { CultureInfo culture; string separator; /// /// Argument to string.Format. /// public string Format = ""; /// /// Unit of the numbers (e.g. 'Hz'). /// public string Unit = ""; /// /// Boolean setting. When true, parse number into prefixes. For example, '10000 Hz' becomes '10 kHz'. /// public bool UsePrefix = false; /// /// Pre-scales numbers before converting. /// public double PreScaling = 1.0; /// /// Boolean setting. When true, numbers are parsed back into ranges. When false, separate values as used as their raw representation. /// public bool UseRanges = true; /// /// Print using compact representation. /// public bool IsCompact = false; /// /// The culture used to parse/write numbers. public NumberFormatter(CultureInfo culture) { if (culture == null) throw new ArgumentNullException("culture"); this.culture = culture; separator = culture.NumberFormat.NumberGroupSeparator + " "; } /// /// Creates a number parser based on a UnitAttribute. /// /// /// public NumberFormatter(CultureInfo culture, UnitAttribute unit) : this(culture) { if (unit != null) { Format = unit.StringFormat; Unit = unit.Unit; UsePrefix = unit.UseEngineeringPrefix; PreScaling = unit.PreScaling; UseRanges = unit.UseRanges; } } const bool enableFeatureFractions = false; BigFloat parseNumber(string trimmed) { // support fractions e.g 1/3 disabled because its hard to convert back from 0.333333... to 1/3, so this gives problems with formatting. if (enableFeatureFractions && trimmed.Contains('/')) return trimmed.Split('/').Select(part => parseNumber(part.Trim())).Aggregate((x, y) => x * PreScaling / y); return UnitFormatter.Parse(trimmed, Unit ?? "", Format, culture) * PreScaling; } bool tryParseNumber(string trimmed, out BigFloat val) { return UnitFormatter.TryParse(trimmed, Unit ?? "", Format, culture, out val); } void parseBackNumber(BigFloat number, StringBuilder sb) { if (PreScaling != 1.0) number = number / PreScaling; UnitFormatter.Format(sb, number , UsePrefix, Unit ?? "", Format, culture, IsCompact); } string parseBackNumber(BigFloat number) { return UnitFormatter.Format(number / PreScaling, UsePrefix, Unit ?? "", Format, culture, IsCompact); } void parseBackRange(BigFloat Start, BigFloat Step, BigFloat Stop, StringBuilder sb) { parseBackNumber(Start, sb); if (Step != PreScaling) { sb.Append(" : "); parseBackNumber(Step, sb); } sb.Append(" : "); parseBackNumber(Stop, sb); } string parseBackRange(Range rng) { if (rng.Step == PreScaling) { return string.Format("{0} : {1}", parseBackNumber(rng.Start), parseBackNumber(rng.Stop)); } return string.Format("{0} : {1} : {2}", parseBackNumber(rng.Start), parseBackNumber(rng.Step), parseBackNumber(rng.Stop)); } Range parseRange(string formatted) { var parts = formatted.Split(':').Select(s => s.Trim()); var rangeitems = parts.Select(parseNumber).ToArray(); Range result = null; if (rangeitems.Length == 3) { result = new Range(rangeitems[0], rangeitems[2], rangeitems[1]); } else if (rangeitems.Length == 2) { result = new Range(rangeitems[0], rangeitems[1], PreScaling * (rangeitems[1] - rangeitems[0]).Sign()); } else { throw new FormatException(string.Format("Unable to parse Range from {0}", formatted)); } result.CheckRange(); return result; } /// /// Parses a string to a sequence of doubles. /// supports ranges, sequences, units and prefixes. /// /// /// public ICombinedNumberSequence Parse(string value) { if (value == null) throw new ArgumentNullException("value"); string separator = culture.NumberFormat.NumberGroupSeparator; var splits = value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries); List> parts = new List>(); foreach (var split in splits) { var trimmed = split.Trim(); if (trimmed.Contains(':')) { Range rng = parseRange(trimmed); parts.Add(rng); } else { var result = parseNumber(trimmed); if (parts.Count == 0 || parts[parts.Count - 1] is Range) { parts.Add(new List { result }); } else { ((IList)parts[parts.Count - 1]).Add(result); } } } List> parts2 = new List>(); if (UseRanges) { // this for loop 'compresses' the parts by trying to reuse and create Range objects. for (int i = 0; i < parts.Count; i++) { var item = parts[i]; if (item is Range) { parts2.Add(item); continue; } var vals = item as IList; bool reuse_last = false; BigFloat start = 0, stop = 0, step = 0; int nitems = 0; var last = parts2.LastOrDefault() as Range; if (last != null) { start = last.Start; stop = last.Stop; step = last.Step; nitems = (int)((((stop - start) / step) + 0.5).Rounded() + 1); reuse_last = true; } Action submit = () => { if (nitems == 0) return; if (reuse_last) { parts2[parts2.Count - 1] = new Range(start, stop, step); } else { if (nitems > 2) { if (step == 0) { BigFloat[] values = new BigFloat[nitems]; for (int j = 0; j < nitems; j++) values[j] = start; parts2.Add(values); } else { parts2.Add(new Range(start, stop, step)); } } else if (nitems == 2) { parts2.Add(new BigFloat[] { start, stop }); } else { parts2.Add(new BigFloat[] { start }); } } nitems = 0; step = 0; }; foreach (var val in vals) { start: if (nitems == 0) { start = val; nitems = 1; continue; } if (nitems == 1) { stop = val; step = stop - start; nitems = 2; continue; } BigFloat nextval; if (step == 0) nextval = stop; // Avoid NaN nextval. else nextval = start + (step * (1 + ((stop - start) / step).Round())); if ((val - nextval) == 0) { stop = val; nitems += 1; } else { submit(); reuse_last = false; goto start; // run again with val as start. } } submit(); } } else parts2 = parts.Select(x => x.ToList() as IEnumerable).ToList(); return new CombinedNumberSequences(parts2).CastTo(); } void pushSeq(StringBuilder sb, BigFloat val) { if (sb.Length != 0) sb.Append(separator); parseBackNumber(val, sb); } void pushSeq(StringBuilder sb, IList seq, BigFloat step) { if (seq.Count > 2 && step.IsZero == false) { if (sb.Length != 0) sb.Append(separator); parseBackRange(seq[0], step, seq[seq.Count - 1], sb); } else { foreach (var val in seq) { if (sb.Length != 0) sb.Append(separator); parseBackNumber(val, sb); } } } /// /// Parses a number back to a string. /// /// /// public string FormatNumber(object value) { if (value == null) return ""; return parseBackNumber(BigFloat.Convert(value)); } /// /// Parses a single number from a string. /// /// /// /// public object ParseNumber(string str, Type t) { if (str == null) throw new ArgumentNullException("str"); if (t == null) throw new ArgumentNullException("t"); try { return parseNumber(str).ConvertTo(t); } catch (OverflowException) { throw new FormatException(string.Format("Unable to parse '{0}' to a {1}", str, t.Name)); } } /// /// Try to parse a single number from a string. /// /// the string to parse. /// the return type of value. must be numeric. /// resulting value. Null if parsing failed. /// public bool TryParseNumber(string str, Type t, out object val) { if(tryParseNumber(str, out BigFloat val2)) { val = val2.ConvertTo(t); return true; } val = null; return false; } /// /// Parses a sequence of numbers back into a string. /// /// /// public string FormatRange(IEnumerable values) { if (values == null) throw new ArgumentNullException("values"); { // Check if values is a ICombinedNumberSequence. // If so, it can be parsed back faster. (without iterating through all the ranges). var seqs = values as _ICombinedNumberSequence; if (seqs != null) { StringBuilder sb = StringBuilderCache.GetStringBuilder(); foreach (var subseq in seqs.Sequences) { var range = subseq as Range; if (range != null) { if (sb.Length != 0) sb.Append(separator); sb.Append(parseBackRange(range)); } else { foreach (var val in subseq) { if (sb.Length != 0) sb.Append(separator); sb.Append(parseBackNumber(val)); } } } return sb.ToString(); } } if (UseRanges) { // Parse the slow way. StringBuilder sb = StringBuilderCache.GetStringBuilder(); List sequence = new List(); BigFloat seq_step = 0; foreach (var _val in values) { var val = BigFloat.Convert(_val); if (sequence.Count < 2) { sequence.Add(val); } else { seq_step = sequence[1] - sequence[0]; var nextVal = sequence[0] + seq_step * (1 + ((sequence.Last() - sequence[0]) / seq_step).Round()); if ((nextVal - val) == BigFloat.Zero) { sequence.Add(val); } else { if (sequence.Count == 2) { pushSeq(sb, sequence[0]); sequence.RemoveAt(0); } else { pushSeq(sb, sequence, seq_step); sequence.Clear(); } sequence.Add(val); } } } pushSeq(sb, sequence, seq_step); return sb.ToString(); } if (!UsePrefix) { // this ca be done really fast since we dont have to use BigFloat. var sb = StringBuilderCache.GetStringBuilder(); foreach (var _val in values) { if (sb.Length != 0) sb.Append(separator); switch (_val) { case float i: sb.Append(i.ToString("R", culture)); break; case decimal i: sb.Append(i.ToString("G", culture)); break; case double i: sb.Append(i.ToString("R17", culture)); break; default: sb.Append(_val); break; } if (string.IsNullOrEmpty(Unit) == false) { sb.Append(" "); sb.Append(Unit); } } return sb.ToString(); } { StringBuilder sb = StringBuilderCache.GetStringBuilder(); foreach (var _val in values) { var val = BigFloat.Convert(_val); if (sb.Length != 0) sb.Append(separator); parseBackNumber(val, sb); } return sb.ToString(); } } } }