From 8554ea7f9a7827e8cf082edaa8d241395e4695f0 Mon Sep 17 00:00:00 2001 From: Dunbaratu Date: Sat, 27 Mar 2021 11:53:16 -0500 Subject: [PATCH] Fixes #2894 via ToStringIndent() Fixes #2894 There are going to be two proposals for how to fix #2894. (Read the comments for the issue). This is one of them. In this proposal, We stop borrowing the Dump infrastructure for creating ToString()s. The Dump infrastructure was designed for when you want to serialize, meaing every single field must be visited and written with nothing 'missing', so values can be saved and restored. I found that layering on top of that system while still preserving the notion of ToString() being meant for human consumption was kind of hard to wrap my head around so I went with this idea. This PR is being made before this is fully tested, so it can be shown to other people who are proposing alternate solutions. I also want to show what this output ends up looking like when you try it. Note the code still has some "eraseme" things in it at this point, which is how I remind myself that an idea wasn't done yet. (I have a check I do before a final PR, usually, where I 'grep' all my code for the string "eraseme" to highlight places I stuck something in temporarily during testing. Here I'm using that to remind myself of stuff that isn't done yet in this PR. While I think this produces good output, I am willing to be convinced other approaches might work too, so I'm open to trying to preserve using Dumper for indented ToString()s if it can be done. --- src/kOS.Safe/Encapsulation/EnumerableValue.cs | 45 ++++++++++++++++++- src/kOS.Safe/Encapsulation/Lexicon.cs | 43 +++++++++++++++++- src/kOS.Safe/Encapsulation/ListValue.cs | 25 +++++++++++ src/kOS.Safe/Encapsulation/QueueValue.cs | 29 ++++++++++++ src/kOS.Safe/Encapsulation/RangeValue.cs | 14 ++++++ src/kOS.Safe/Encapsulation/StackValue.cs | 26 +++++++++++ src/kOS.Safe/Encapsulation/Structure.cs | 24 ++++++++++ src/kOS.Safe/Encapsulation/UniqueSetValue.cs | 26 ++++++++++- .../Serialization/TerminalFormatter.cs | 14 +++++- src/kOS.Safe/Utilities/kosMath.cs | 22 +++++++++ 10 files changed, 263 insertions(+), 5 deletions(-) diff --git a/src/kOS.Safe/Encapsulation/EnumerableValue.cs b/src/kOS.Safe/Encapsulation/EnumerableValue.cs index 23a31a207d..66df27a093 100644 --- a/src/kOS.Safe/Encapsulation/EnumerableValue.cs +++ b/src/kOS.Safe/Encapsulation/EnumerableValue.cs @@ -1,5 +1,6 @@ using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Serialization; +using System.Text; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -37,11 +38,53 @@ public bool Contains(T item) return InnerEnumerable.Contains(item); } + public override string ToStringIndented(int level) + { + if (level >= TerminalFormatter.MAX_INDENT_LEVEL) + return "<>"; + + StringBuilder sb = new StringBuilder(); + string pad = string.Empty.PadRight(level * TerminalFormatter.INDENT_SPACES, ' '); + + int cnt = this.Count(); + if (cnt == 0) + sb.Append(string.Format("{0} (empty)", KOSName)); + else if (cnt == 1) + sb.Append(string.Format("{0} of 1 item:", KOSName)); + else + sb.Append(string.Format("{0} of {1} items:", KOSName, cnt)); + + sb.Append(string.Format("\n{0}",ToStringItems(level + 1))); + return sb.ToString(); + } + public override string ToString() { - return new SafeSerializationMgr(null).ToString(this); + StringBuilder sb = new StringBuilder(); + + int cnt = this.Count(); + if (cnt == 0) + sb.Append(string.Format("{0} (empty)", KOSName)); + else if (cnt == 1) + sb.Append(string.Format("{0} of 1 item:", KOSName)); + else + sb.Append(string.Format("{0} of {1} items:", KOSName, cnt)); + + sb.Append(string.Format("\n{0}",ToStringItems(1))); + return sb.ToString(); } + /// + /// Print the inner items (not the header) of a container. Override this and this + /// enumerable structure will use it in its ToString() and its ToStringIndented(). + /// IMPORTANT: If your enumerable contains zero things, then return empty string, not even + /// a newline. If your enumerable contains at least one thing, then print + /// a line break at the end of each thing. + /// + /// you must pad all lines with this level of indent*TerminalFormatter.INDENT_SPACES + /// + public abstract string ToStringItems(int level); + public override Dump Dump() { var result = new DumpWithHeader diff --git a/src/kOS.Safe/Encapsulation/Lexicon.cs b/src/kOS.Safe/Encapsulation/Lexicon.cs index e621281051..d959a17c3b 100644 --- a/src/kOS.Safe/Encapsulation/Lexicon.cs +++ b/src/kOS.Safe/Encapsulation/Lexicon.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Text; using kOS.Safe.Utilities; using kOS.Safe.Function; @@ -297,9 +298,49 @@ public void SetIndex(int index, Structure value) internalDictionary[FromPrimitiveWithAssert(index)] = value; } + public override string ToStringIndented(int level) + { + // eraseme - I NOTICED I REPEATED THIS EXACT SNIP OF CODE CUT-N-PASTED A FEW TIMES + // eraseme - IN DIFFERENT PLACES. THIS IS PROBABLY A CANDIDATE FOR MAKING INTO ONE + // eraseme - COMMON UTILITY METHOD (THE PART THAT PRINTS THIS HEADER WHEN A ToStringIndented() + // eraseme - WANTS TO). + if (level >= TerminalFormatter.MAX_INDENT_LEVEL) + return "<>"; + + StringBuilder sb = new StringBuilder(); + string pad = string.Empty.PadRight(level * TerminalFormatter.INDENT_SPACES, ' '); + + int cnt = this.Count(); + if (cnt == 0) + sb.Append(string.Format("{0} (empty)", KOSName)); + else if (cnt == 1) + sb.Append(string.Format("{0} of 1 item:", KOSName)); + else + sb.Append(string.Format("{0} of {1} items:", KOSName, cnt)); + + sb.Append(string.Format("\n{0}", ToStringItems(level + 1))); + return sb.ToString(); + } + + public string ToStringItems(int level) + { + StringBuilder sb = new StringBuilder(); + string pad = string.Empty.PadRight(level * TerminalFormatter.INDENT_SPACES, ' '); + foreach (Structure key in internalDictionary.Keys) + { + Structure val = internalDictionary[key]; + sb.Append(string.Format("{0}[{1}] = {2}\n", + pad, + key.ToString(), + val.ToStringIndented(level) + )); + } + return sb.ToString(); + } + public override string ToString() { - return new SafeSerializationMgr(null).ToString(this); + return ToStringIndented(0); } // Try to call the normal SetSuffix that all structures do, but if that fails, diff --git a/src/kOS.Safe/Encapsulation/ListValue.cs b/src/kOS.Safe/Encapsulation/ListValue.cs index 5650380282..a204be7757 100644 --- a/src/kOS.Safe/Encapsulation/ListValue.cs +++ b/src/kOS.Safe/Encapsulation/ListValue.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Exceptions; using kOS.Safe.Properties; @@ -73,6 +74,30 @@ public override void LoadDump(Dump dump) } } + public override string ToStringItems(int level) + { + StringBuilder sb = new StringBuilder(); + string pad = string.Empty.PadRight(level * TerminalFormatter.INDENT_SPACES, ' '); + int cnt = this.Count(); + int digitWidth = Utilities.KOSMath.DecimalDigitsIn(cnt); + for (int i = 0; i < cnt; ++i) + { + Structure asStructure = this[i] as Structure; + if (asStructure != null) + { + sb.Append(string.Format("{0}[{1}] = {2}\n", + pad, + i.ToString().PadLeft(digitWidth), + asStructure.ToStringIndented(level) + )); + } + else // Hypothetically this case should not happen, but if we screwed up somewhere so it does, at least you can see something. + { + sb.Append(this[i].ToString()); + } + } + return sb.ToString(); + } private void ListInitializeSuffixes() { AddSuffix("COPY", new NoArgsSuffix> (() => new ListValue(this))); diff --git a/src/kOS.Safe/Encapsulation/QueueValue.cs b/src/kOS.Safe/Encapsulation/QueueValue.cs index 9c91ac7e3e..ee9f252504 100644 --- a/src/kOS.Safe/Encapsulation/QueueValue.cs +++ b/src/kOS.Safe/Encapsulation/QueueValue.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Serialization; using kOS.Safe.Function; @@ -63,6 +64,33 @@ public static QueueValue CreateQueue(IEnumerable list) { return new QueueValue(list.Cast()); } + + public override string ToStringItems(int level) + { + StringBuilder sb = new StringBuilder(); + string pad = string.Empty.PadRight(level * TerminalFormatter.INDENT_SPACES, ' '); + var asArray = InnerEnumerable.ToArray(); + int i = 0; + foreach (object item in asArray) + { + Structure asStructure = item as Structure; + if (asStructure != null) + { + sb.Append(string.Format("{0}[{1}] = {2}\n", + pad, + (i == 0 ? "top ->" : (i == asArray.Count() ? "bottom" : " ")), + asStructure.ToStringIndented(level) + )); + } + else // Hypothetically this case should not happen, but if we screwed up somewhere so it does, at least you can see something. + { + sb.Append(item.ToString()); + } + ++i; + } + return sb.ToString(); + } + } [kOS.Safe.Utilities.KOSNomenclature("Queue", KOSToCSharp = false)] // one-way because the generic templated QueueValue is the canonical one. @@ -110,5 +138,6 @@ private void InitializeSuffixes() { return new QueueValue(toCopy.Select(x => FromPrimitiveWithAssert(x))); } + } } \ No newline at end of file diff --git a/src/kOS.Safe/Encapsulation/RangeValue.cs b/src/kOS.Safe/Encapsulation/RangeValue.cs index 79a621a4fc..49e8c7e76b 100644 --- a/src/kOS.Safe/Encapsulation/RangeValue.cs +++ b/src/kOS.Safe/Encapsulation/RangeValue.cs @@ -85,6 +85,20 @@ public override string ToString() { return "RANGE(" + InnerEnumerable.Start + ", " + InnerEnumerable.Stop + ", " + InnerEnumerable.Step + ")"; } + + public override string ToStringIndented(int level) + { + // By default, an Enumerable's ToStringIndented() would print out a header line, but here it's + // not needed, so override it to just print the content line only: + return ToStringItems(level); + } + public override string ToStringItems(int level) + { + // Indent level is being ignored because this is single-line and + // never contains other things, despite being implemented as + // a EnumerableValue which needs a ToStringItems(). + return ToString(); + } } public class Range : IEnumerable diff --git a/src/kOS.Safe/Encapsulation/StackValue.cs b/src/kOS.Safe/Encapsulation/StackValue.cs index 1eed16dae6..7d2bddcdc3 100644 --- a/src/kOS.Safe/Encapsulation/StackValue.cs +++ b/src/kOS.Safe/Encapsulation/StackValue.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Serialization; using kOS.Safe.Function; @@ -69,6 +70,31 @@ public static StackValue CreateStack(IEnumerable list) { return new StackValue(list.Cast()); } + public override string ToStringItems(int level) + { + StringBuilder sb = new StringBuilder(); + string pad = string.Empty.PadRight(level * TerminalFormatter.INDENT_SPACES, ' '); + var asArray = InnerEnumerable.ToArray(); + int i = 0; + foreach (object item in asArray) + { + Structure asStructure = item as Structure; + if (asStructure != null) + { + sb.Append(string.Format("{0}[{1}] = {2}\n", + pad, + (i == 0 ? "front->" : (i == asArray.Count() ? "back ->" : " ")), + asStructure.ToStringIndented(level) + )); + } + else // Hypothetically this case should not happen, but if we screwed up somewhere so it does, at least you can see something. + { + sb.Append(item.ToString()); + } + ++i; + } + return sb.ToString(); + } } [kOS.Safe.Utilities.KOSNomenclature("Stack", KOSToCSharp = false)] // one-way because the generic templated StackValue is the canonical one. diff --git a/src/kOS.Safe/Encapsulation/Structure.cs b/src/kOS.Safe/Encapsulation/Structure.cs index 3027491585..6d29359d73 100644 --- a/src/kOS.Safe/Encapsulation/Structure.cs +++ b/src/kOS.Safe/Encapsulation/Structure.cs @@ -358,5 +358,29 @@ public static object ToPrimitive(object value) return value; } + + /// + /// A wrapper around Structure.ToString() that will indent the ToString() output + /// to the desired indent level. + /// + public virtual string ToStringIndented(int level) + { + if (level >= TerminalFormatter.MAX_INDENT_LEVEL) + return "<>"; + + StringBuilder returnVal = new StringBuilder(); + string[] lines = ToString().Split('\n'); + string pad = ""; + if (lines.Count() > 1) + { + pad = String.Empty.PadRight(level * TerminalFormatter.INDENT_SPACES, ' '); + returnVal.Append("\n"); + } + foreach (string line in lines) + { + returnVal.AppendFormat("{0}{1}", pad, line); + } + return returnVal.ToString(); + } } } diff --git a/src/kOS.Safe/Encapsulation/UniqueSetValue.cs b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs index 4e0e1c0da9..04d92834b6 100644 --- a/src/kOS.Safe/Encapsulation/UniqueSetValue.cs +++ b/src/kOS.Safe/Encapsulation/UniqueSetValue.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Exceptions; using kOS.Safe.Serialization; @@ -67,7 +68,30 @@ private void SetInitializeSuffixes() AddSuffix("COPY", new NoArgsSuffix> (() => new UniqueSetValue(this))); AddSuffix("ADD", new OneArgsSuffix (toAdd => Collection.Add(toAdd))); AddSuffix("REMOVE", new OneArgsSuffix (toRemove => Collection.Remove(toRemove))); - } + } + + public override string ToStringItems(int level) + { + StringBuilder sb = new StringBuilder(); + string pad = string.Empty.PadRight(level * TerminalFormatter.INDENT_SPACES, ' '); + var asArray = InnerEnumerable.ToArray(); + foreach (object item in asArray) + { + Structure asStructure = item as Structure; + if (asStructure != null) + { + sb.Append(string.Format("{0}{1}\n", + pad, + asStructure.ToStringIndented(level) + )); + } + else // Hypothetically this case should not happen, but if we screwed up somewhere so it does, at least you can see something. + { + sb.Append(item.ToString()); + } + } + return sb.ToString(); + } } [kOS.Safe.Utilities.KOSNomenclature("UniqueSet", KOSToCSharp = false)] // one-way because the generic templated UniqueSetValue is the canonical one. diff --git a/src/kOS.Safe/Serialization/TerminalFormatter.cs b/src/kOS.Safe/Serialization/TerminalFormatter.cs index b14448b4af..ba4f8ed7a7 100644 --- a/src/kOS.Safe/Serialization/TerminalFormatter.cs +++ b/src/kOS.Safe/Serialization/TerminalFormatter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using kOS.Safe.Encapsulation; using System.Linq; @@ -7,7 +7,17 @@ namespace kOS.Safe.Serialization { public class TerminalFormatter : IFormatWriter { - private static int INDENT_SPACES = 2; + public static int INDENT_SPACES = 2; + + // eraseme - MUST INCREASE THIS VALUE AFTER TESTING IS OVER!!! + public static int MAX_INDENT_LEVEL = 5; // SET LOW DURING TESTING SO IT'S EASY TO TRIGGER IT + + // eraseme - THIS ENTIRE CLASS BELOW THIS POINT IS PROBABLY NOT NEEDED ANYMORE IF THIS PR IS USED. + // eraseme - IT ONLY USES THE ABOVE TWO SETTINGS (TEST THIS BY REMOVING THE REST OF THIS AND + // eraseme - SEEING IF IT COMPILES.) + // eraseme - THE ENTIRE CLASS COULD GO AWAY AND THESE SETTINGS COULD BE MOVED ELSEWHERE, + // eraseme - WHERE A USER SCRIPT COULD ALTER THEM. + private static readonly TerminalFormatter instance; public static TerminalFormatter Instance diff --git a/src/kOS.Safe/Utilities/kosMath.cs b/src/kOS.Safe/Utilities/kosMath.cs index ad47b7301b..658ddbe05a 100644 --- a/src/kOS.Safe/Utilities/kosMath.cs +++ b/src/kOS.Safe/Utilities/kosMath.cs @@ -147,5 +147,27 @@ public static double GetRandom(string key) randomizers.Add(key, new Random()); return randomizers[key].NextDouble(); } + + /// + /// Get the number of decimal digits in a number. i.e. if input = 33333, return a 5. + /// + /// + /// + public static int DecimalDigitsIn(int val) + { + int absVal = val < 0 ? -val : val; + // Believe it or not, a basic hardcoded if-else chain is actually the fastest performance + // when you know the numbers aren't allowed to be large (have to fit in int32): + if (absVal < 10) return 1; + if (absVal < 100) return 2; + if (absVal < 1000) return 3; + if (absVal < 10000) return 4; + if (absVal < 100000) return 5; + if (absVal < 1000000) return 6; + if (absVal < 10000000) return 7; + if (absVal < 100000000) return 8; + if (absVal < 1000000000) return 9; + return 10; + } } }