From 26cbd4cc5db200598788774ef8c2795e2b7efcda Mon Sep 17 00:00:00 2001 From: molsonkiko <46202915+molsonkiko@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:05:54 -0800 Subject: [PATCH] improve regex form, enum schema val, add tests 1. add regex search form (see docs/README.md) 2. fix bug with JSON schema validation where JSON schema is invalid 3. allow JSON schemas to have "enum" keyword w/o "type" keyword or with an array of types 4. add caching of s_csv and s_fa results for intermediate-size input 5. add to_csv non-vectorized function for CSV export (tabular JSON only) --- CHANGELOG.md | 13 +- .../Forms/RegexSearchForm.Designer.cs | 189 +++++++++- JsonToolsNppPlugin/Forms/RegexSearchForm.cs | 329 +++++++----------- JsonToolsNppPlugin/Forms/TreeViewer.cs | 50 ++- JsonToolsNppPlugin/JSONTools/JNode.cs | 15 +- JsonToolsNppPlugin/JSONTools/JsonParser.cs | 34 ++ .../JSONTools/JsonSchemaValidator.cs | 65 ++-- .../JSONTools/JsonTabularize.cs | 2 +- JsonToolsNppPlugin/JSONTools/RemesPath.cs | 9 +- .../JSONTools/RemesPathFunctions.cs | 203 ++++++++++- .../JSONTools/RemesPathLexer.cs | 3 + JsonToolsNppPlugin/Main.cs | 144 ++++---- JsonToolsNppPlugin/Properties/AssemblyInfo.cs | 4 +- JsonToolsNppPlugin/Tests/Benchmarker.cs | 2 +- .../Tests/JsonSchemaValidatorTests.cs | 21 ++ JsonToolsNppPlugin/Tests/RemesPathTests.cs | 55 +++ .../Tests/UserInterfaceTests.cs | 119 ++++++- docs/README.md | 21 +- docs/RemesPath.md | 14 +- docs/regex search form csv example.PNG | Bin 0 -> 86648 bytes docs/regex search form regex example.PNG | Bin 0 -> 32252 bytes most recent errors.txt | 88 ++--- 22 files changed, 992 insertions(+), 388 deletions(-) create mode 100644 docs/regex search form csv example.PNG create mode 100644 docs/regex search form regex example.PNG diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ccdd5e..a7a6f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,11 +45,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added 1. Option to customize which [toolbar icons](/docs/README.md#toolbar-icons) are displayed, and their order. -2. [For loops in RemesPath](/docs/RemesPath.md#for-loopsloop-variables-added-in-v60) -3. [`bool, s_csv` and `s_fa` vectorized arg functions](/docs/RemesPath.md#vectorized-functions) and [`randint` and `csv_regex` non-vectorized arg functions](/docs/RemesPath.md#non-vectorized-functions) to RemesPath. -4. Make second argument of [`s_split` RemesPath function](/docs/RemesPath.md#vectorized-functions) optional; 1-argument variant splits on whitespace. -5. Right-click dropdown menu in [error form](/docs/README.md#error-form-and-status-bar), allowing export of errors to JSON or refreshing the form. -6. The parser is now much better at recovering when an object is missing its closing `'}'` or an array is missing its closing `']'`. +2. New [regex search form](/docs/README.md#regex-search-form) for using treeview to see regex search results in any file +3. [For loops in RemesPath](/docs/RemesPath.md#for-loopsloop-variables-added-in-v60) +4. [`bool`, `s_csv` and `s_fa` vectorized arg functions](/docs/RemesPath.md#vectorized-functions) and [`randint`, `csv_regex`, and `to_csv` non-vectorized arg functions](/docs/RemesPath.md#non-vectorized-functions) to RemesPath. +5. Make second argument of [`s_split` RemesPath function](/docs/RemesPath.md#vectorized-functions) optional; 1-argument variant splits on whitespace. +6. Right-click dropdown menu in [error form](/docs/README.md#error-form-and-status-bar), allowing export of errors to JSON or refreshing the form. +7. The parser is now much better at recovering when an object is missing its closing `'}'` or an array is missing its closing `']'`. +8. Support for [JSON Schema validation](/docs/README.md#validating-json-against-json-schema) of `enum` keyword where the `type` is missing or an array. ### Changed @@ -69,6 +71,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). 7. unnecessary prompt when manually reloading [error form](/docs/README.md#error-form-and-status-bar) 8. issue with trying to view error form when the error form was already open 9. RemesPath backtick strings now can have a literal `\` character just before the closing backtick. Previously this was impossible because of a regex-writing bug. +10. Eliminated plugin crash when attempting to validate with an invalid JSON schema. Now a message box will show in that situation. ## [5.8.0] - 2023-10-09 diff --git a/JsonToolsNppPlugin/Forms/RegexSearchForm.Designer.cs b/JsonToolsNppPlugin/Forms/RegexSearchForm.Designer.cs index 842315a..4ac4abf 100644 --- a/JsonToolsNppPlugin/Forms/RegexSearchForm.Designer.cs +++ b/JsonToolsNppPlugin/Forms/RegexSearchForm.Designer.cs @@ -34,15 +34,27 @@ private void InitializeComponent() this.SearchButton = new System.Windows.Forms.Button(); this.RegexTextBoxLabel = new System.Windows.Forms.Label(); this.Title = new System.Windows.Forms.Label(); - this.NumGroupsTextBox = new System.Windows.Forms.TextBox(); + this.ColumnsToParseAsNumberTextBox = new System.Windows.Forms.TextBox(); this.NumGroupsTextBoxLabel = new System.Windows.Forms.Label(); + this.ParseAsCsvCheckBox = new System.Windows.Forms.CheckBox(); + this.DelimiterTextBox = new System.Windows.Forms.TextBox(); + this.DelimiterTextBoxLabel = new System.Windows.Forms.Label(); + this.QuoteCharTextBox = new System.Windows.Forms.TextBox(); + this.QuoteCharTextBoxLabel = new System.Windows.Forms.Label(); + this.NewlineComboBox = new System.Windows.Forms.ComboBox(); + this.NewlineComboBoxLabel = new System.Windows.Forms.Label(); + this.HeaderHandlingComboBox = new System.Windows.Forms.ComboBox(); + this.HeaderHandlingComboBoxLabel = new System.Windows.Forms.Label(); + this.IncludeFullMatchAsFirstItemCheckBox = new System.Windows.Forms.CheckBox(); + this.NColumnsTextBoxLabel = new System.Windows.Forms.Label(); + this.NColumnsTextBox = new System.Windows.Forms.TextBox(); this.SuspendLayout(); // // RegexTextBox // this.RegexTextBox.Location = new System.Drawing.Point(12, 52); this.RegexTextBox.Name = "RegexTextBox"; - this.RegexTextBox.Size = new System.Drawing.Size(394, 22); + this.RegexTextBox.Size = new System.Drawing.Size(655, 22); this.RegexTextBox.TabIndex = 0; // // IgnoreCaseCheckBox @@ -60,7 +72,7 @@ private void InitializeComponent() // SearchButton // this.SearchButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); - this.SearchButton.Location = new System.Drawing.Point(226, 162); + this.SearchButton.Location = new System.Drawing.Point(354, 245); this.SearchButton.Name = "SearchButton"; this.SearchButton.Size = new System.Drawing.Size(63, 23); this.SearchButton.TabIndex = 2; @@ -71,7 +83,7 @@ private void InitializeComponent() // RegexTextBoxLabel // this.RegexTextBoxLabel.AutoSize = true; - this.RegexTextBoxLabel.Location = new System.Drawing.Point(412, 55); + this.RegexTextBoxLabel.Location = new System.Drawing.Point(673, 55); this.RegexTextBoxLabel.Name = "RegexTextBoxLabel"; this.RegexTextBoxLabel.Size = new System.Drawing.Size(86, 16); this.RegexTextBoxLabel.TabIndex = 3; @@ -81,35 +93,170 @@ private void InitializeComponent() // this.Title.AutoSize = true; this.Title.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.Title.Location = new System.Drawing.Point(139, 9); + this.Title.Location = new System.Drawing.Point(264, 9); this.Title.Name = "Title"; this.Title.Size = new System.Drawing.Size(217, 22); this.Title.TabIndex = 4; this.Title.Text = "Regex Search to JSON"; // - // NumGroupsTextBox + // ColumnsToParseAsNumberTextBox // - this.NumGroupsTextBox.Location = new System.Drawing.Point(12, 127); - this.NumGroupsTextBox.Name = "NumGroupsTextBox"; - this.NumGroupsTextBox.Size = new System.Drawing.Size(257, 22); - this.NumGroupsTextBox.TabIndex = 5; + this.ColumnsToParseAsNumberTextBox.Location = new System.Drawing.Point(12, 210); + this.ColumnsToParseAsNumberTextBox.Name = "ColumnsToParseAsNumberTextBox"; + this.ColumnsToParseAsNumberTextBox.Size = new System.Drawing.Size(257, 22); + this.ColumnsToParseAsNumberTextBox.TabIndex = 5; // // NumGroupsTextBoxLabel // this.NumGroupsTextBoxLabel.AutoSize = true; - this.NumGroupsTextBoxLabel.Location = new System.Drawing.Point(271, 130); + this.NumGroupsTextBoxLabel.Location = new System.Drawing.Point(271, 213); this.NumGroupsTextBoxLabel.Name = "NumGroupsTextBoxLabel"; this.NumGroupsTextBoxLabel.Size = new System.Drawing.Size(227, 16); this.NumGroupsTextBoxLabel.TabIndex = 6; this.NumGroupsTextBoxLabel.Text = "Groups to parse as number (int array)"; // + // ParseAsCsvCheckBox + // + this.ParseAsCsvCheckBox.AutoSize = true; + this.ParseAsCsvCheckBox.Location = new System.Drawing.Point(12, 127); + this.ParseAsCsvCheckBox.Name = "ParseAsCsvCheckBox"; + this.ParseAsCsvCheckBox.Size = new System.Drawing.Size(120, 20); + this.ParseAsCsvCheckBox.TabIndex = 7; + this.ParseAsCsvCheckBox.Text = "Parse as CSV?"; + this.ParseAsCsvCheckBox.UseVisualStyleBackColor = true; + this.ParseAsCsvCheckBox.CheckedChanged += new System.EventHandler(this.ParseAsCsvCheckBox_CheckedChanged); + // + // DelimiterTextBox + // + this.DelimiterTextBox.Location = new System.Drawing.Point(205, 127); + this.DelimiterTextBox.Name = "DelimiterTextBox"; + this.DelimiterTextBox.Size = new System.Drawing.Size(22, 22); + this.DelimiterTextBox.TabIndex = 8; + this.DelimiterTextBox.Text = ","; + this.DelimiterTextBox.Visible = false; + // + // DelimiterTextBoxLabel + // + this.DelimiterTextBoxLabel.AutoSize = true; + this.DelimiterTextBoxLabel.Location = new System.Drawing.Point(233, 131); + this.DelimiterTextBoxLabel.Name = "DelimiterTextBoxLabel"; + this.DelimiterTextBoxLabel.Size = new System.Drawing.Size(60, 16); + this.DelimiterTextBoxLabel.TabIndex = 9; + this.DelimiterTextBoxLabel.Text = "Delimiter"; + this.DelimiterTextBoxLabel.Visible = false; + // + // QuoteCharTextBox + // + this.QuoteCharTextBox.Location = new System.Drawing.Point(304, 127); + this.QuoteCharTextBox.Name = "QuoteCharTextBox"; + this.QuoteCharTextBox.Size = new System.Drawing.Size(22, 22); + this.QuoteCharTextBox.TabIndex = 10; + this.QuoteCharTextBox.Text = "\""; + this.QuoteCharTextBox.Visible = false; + // + // QuoteCharTextBoxLabel + // + this.QuoteCharTextBoxLabel.AutoSize = true; + this.QuoteCharTextBoxLabel.Location = new System.Drawing.Point(332, 131); + this.QuoteCharTextBoxLabel.Name = "QuoteCharTextBoxLabel"; + this.QuoteCharTextBoxLabel.Size = new System.Drawing.Size(102, 16); + this.QuoteCharTextBoxLabel.TabIndex = 11; + this.QuoteCharTextBoxLabel.Text = "Quote character"; + this.QuoteCharTextBoxLabel.Visible = false; + // + // NewlineComboBox + // + this.NewlineComboBox.FormattingEnabled = true; + this.NewlineComboBox.Items.AddRange(new object[] { + "CR LF", + "LF", + "CR"}); + this.NewlineComboBox.Location = new System.Drawing.Point(444, 127); + this.NewlineComboBox.Name = "NewlineComboBox"; + this.NewlineComboBox.Size = new System.Drawing.Size(68, 24); + this.NewlineComboBox.TabIndex = 12; + this.NewlineComboBox.Visible = false; + // + // NewlineComboBoxLabel + // + this.NewlineComboBoxLabel.AutoSize = true; + this.NewlineComboBoxLabel.Location = new System.Drawing.Point(518, 131); + this.NewlineComboBoxLabel.Name = "NewlineComboBoxLabel"; + this.NewlineComboBoxLabel.Size = new System.Drawing.Size(55, 16); + this.NewlineComboBoxLabel.TabIndex = 13; + this.NewlineComboBoxLabel.Text = "Newline"; + this.NewlineComboBoxLabel.Visible = false; + // + // HeaderHandlingComboBox + // + this.HeaderHandlingComboBox.FormattingEnabled = true; + this.HeaderHandlingComboBox.Items.AddRange(new object[] { + "Skip header", + "Include header", + "Use header as keys"}); + this.HeaderHandlingComboBox.Location = new System.Drawing.Point(304, 169); + this.HeaderHandlingComboBox.Name = "HeaderHandlingComboBox"; + this.HeaderHandlingComboBox.Size = new System.Drawing.Size(155, 24); + this.HeaderHandlingComboBox.TabIndex = 14; + this.HeaderHandlingComboBox.Visible = false; + // + // HeaderHandlingComboBoxLabel + // + this.HeaderHandlingComboBoxLabel.AutoSize = true; + this.HeaderHandlingComboBoxLabel.Location = new System.Drawing.Point(465, 172); + this.HeaderHandlingComboBoxLabel.Name = "HeaderHandlingComboBoxLabel"; + this.HeaderHandlingComboBoxLabel.Size = new System.Drawing.Size(107, 16); + this.HeaderHandlingComboBoxLabel.TabIndex = 15; + this.HeaderHandlingComboBoxLabel.Text = "Header handling"; + this.HeaderHandlingComboBoxLabel.Visible = false; + // + // IncludeFullMatchAsFirstItemCheckBox + // + this.IncludeFullMatchAsFirstItemCheckBox.AutoSize = true; + this.IncludeFullMatchAsFirstItemCheckBox.Location = new System.Drawing.Point(138, 90); + this.IncludeFullMatchAsFirstItemCheckBox.Name = "IncludeFullMatchAsFirstItemCheckBox"; + this.IncludeFullMatchAsFirstItemCheckBox.Size = new System.Drawing.Size(229, 20); + this.IncludeFullMatchAsFirstItemCheckBox.TabIndex = 16; + this.IncludeFullMatchAsFirstItemCheckBox.Text = "Include full match text as first item?"; + this.IncludeFullMatchAsFirstItemCheckBox.UseVisualStyleBackColor = true; + // + // NColumnsTextBoxLabel + // + this.NColumnsTextBoxLabel.AutoSize = true; + this.NColumnsTextBoxLabel.Location = new System.Drawing.Point(171, 172); + this.NColumnsTextBoxLabel.Name = "NColumnsTextBoxLabel"; + this.NColumnsTextBoxLabel.Size = new System.Drawing.Size(122, 16); + this.NColumnsTextBoxLabel.TabIndex = 17; + this.NColumnsTextBoxLabel.Text = "Number of columns"; + this.NColumnsTextBoxLabel.Visible = false; + // + // NColumnsTextBox + // + this.NColumnsTextBox.Location = new System.Drawing.Point(108, 169); + this.NColumnsTextBox.Name = "NColumnsTextBox"; + this.NColumnsTextBox.Size = new System.Drawing.Size(57, 22); + this.NColumnsTextBox.TabIndex = 18; + this.NColumnsTextBox.Visible = false; + // // RegexSearchForm // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(510, 197); + this.ClientSize = new System.Drawing.Size(771, 280); + this.Controls.Add(this.NColumnsTextBox); + this.Controls.Add(this.NColumnsTextBoxLabel); + this.Controls.Add(this.IncludeFullMatchAsFirstItemCheckBox); + this.Controls.Add(this.HeaderHandlingComboBoxLabel); + this.Controls.Add(this.HeaderHandlingComboBox); + this.Controls.Add(this.NewlineComboBoxLabel); + this.Controls.Add(this.NewlineComboBox); + this.Controls.Add(this.QuoteCharTextBoxLabel); + this.Controls.Add(this.QuoteCharTextBox); + this.Controls.Add(this.DelimiterTextBoxLabel); + this.Controls.Add(this.DelimiterTextBox); + this.Controls.Add(this.ParseAsCsvCheckBox); this.Controls.Add(this.NumGroupsTextBoxLabel); - this.Controls.Add(this.NumGroupsTextBox); + this.Controls.Add(this.ColumnsToParseAsNumberTextBox); this.Controls.Add(this.Title); this.Controls.Add(this.RegexTextBoxLabel); this.Controls.Add(this.SearchButton); @@ -127,10 +274,22 @@ private void InitializeComponent() private System.Windows.Forms.TextBox RegexTextBox; private System.Windows.Forms.CheckBox IgnoreCaseCheckBox; - private System.Windows.Forms.Button SearchButton; private System.Windows.Forms.Label RegexTextBoxLabel; private System.Windows.Forms.Label Title; - private System.Windows.Forms.TextBox NumGroupsTextBox; + private System.Windows.Forms.TextBox ColumnsToParseAsNumberTextBox; private System.Windows.Forms.Label NumGroupsTextBoxLabel; + private System.Windows.Forms.CheckBox ParseAsCsvCheckBox; + private System.Windows.Forms.TextBox DelimiterTextBox; + private System.Windows.Forms.Label DelimiterTextBoxLabel; + private System.Windows.Forms.TextBox QuoteCharTextBox; + private System.Windows.Forms.Label QuoteCharTextBoxLabel; + private System.Windows.Forms.ComboBox NewlineComboBox; + private System.Windows.Forms.Label NewlineComboBoxLabel; + private System.Windows.Forms.ComboBox HeaderHandlingComboBox; + private System.Windows.Forms.Label HeaderHandlingComboBoxLabel; + private System.Windows.Forms.CheckBox IncludeFullMatchAsFirstItemCheckBox; + private System.Windows.Forms.Label NColumnsTextBoxLabel; + private System.Windows.Forms.TextBox NColumnsTextBox; + internal System.Windows.Forms.Button SearchButton; } } \ No newline at end of file diff --git a/JsonToolsNppPlugin/Forms/RegexSearchForm.cs b/JsonToolsNppPlugin/Forms/RegexSearchForm.cs index f4dcef8..1edb60d 100644 --- a/JsonToolsNppPlugin/Forms/RegexSearchForm.cs +++ b/JsonToolsNppPlugin/Forms/RegexSearchForm.cs @@ -1,244 +1,177 @@ using Kbg.NppPluginNET; -using Kbg.NppPluginNET.PluginInfrastructure; using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; using JSON_Tools.Utils; using JSON_Tools.JSON_Tools; -using System.Globalization; +using System.Linq; +using System.Collections.Generic; namespace JSON_Tools.Forms { public partial class RegexSearchForm : Form { - public string regex; private JsonParser jsonParser; + private JsonSchemaValidator.ValidationFunc settingsValidator; public RegexSearchForm() { InitializeComponent(); - jsonParser = new JsonParser(); + FormStyle.ApplyStyle(this, Main.settings.use_npp_styling); + HeaderHandlingComboBox.SelectedIndex = 0; + NewlineComboBox.SelectedIndex = 0; + jsonParser = new JsonParser(LoggerLevel.JSON5, false, false, false, false); + settingsValidator = JsonSchemaValidator.CompileValidationFunc(new JsonParser().Parse( + "{\r\n" + + "\t\"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + "\t\"anyOf\": [\r\n" + + "\t\t{\r\n" + // somewhat kludgy: if "csv" is true, follow this schema + "\t\t\t\"type\": \"object\",\r\n" + + "\t\t\t\"required\": [\"csv\", \"delim\", \"header\", \"nColumns\", \"newline\", \"quote\"],\r\n" + + "\t\t\t\"properties\": {\r\n" + + "\t\t\t\t\"csv\": {\"enum\": [true]},\r\n" + // sets the ParseCsvButton + "\t\t\t\t\"delim\": {\"type\": \"string\", \"minLength\": 1, \"maxLength\": 2},\r\n" + // sets the delimiter text box + "\t\t\t\t\"newline\": {\"enum\": [\"\\r\\n\", \"\\r\", \"\\n\", 0, 1, 2]},\r\n" + // sets the newline combo box (0 and "\r\n" map to "\r\n", 1 and "\n" map to "\n", 2 and "\r" map to "\r") + "\t\t\t\t\"header\": {\"enum\": [0, 1, 2, \"n\", \"d\", \"h\"]},\r\n" + // sets the header handling combo box (0 and "n" map to skipping header, 1 and "h" map to including header, 2 and "d" map to using header as keys) + "\t\t\t\t\"nColumns\": {\"type\": \"integer\", \"exclusiveMinimum\": 0},\r\n" + // nColumns text box + "\t\t\t\t\"quote\": {\"type\": \"string\", \"minLength\": 1, \"maxLength\": 1},\r\n" + // quote box + "\t\t\t\t\"numCols\": {\"type\": \"array\", \"items\": {\"type\": \"integer\"}, \"minItems\": 1}\r\n" + // column numbers to parse as number (omit instead of using 0-length array) + "\t\t\t}\r\n" + + "\t\t},\r\n" + + "\t\t{\r\n" + // and if "csv" is false, follow this schema + "\t\t\t\"type\": \"object\",\r\n" + + "\t\t\t\"required\": [\"csv\", \"regex\", \"fullMatch\", \"ignoreCase\"],\r\n" + + "\t\t\t\"properties\": {\r\n" + + "\t\t\t\t\"csv\": {\"enum\": [false]},\r\n" + // same as first branch + "\t\t\t\t\"regex\": {\"type\": \"string\", \"minLength\": 1},\r\n" + // regex text box + "\t\t\t\t\"ignoreCase\": {\"type\": \"boolean\"},\r\n" + // ignore case checkbox + "\t\t\t\t\"fullMatch\": {\"type\": \"boolean\"},\r\n" + // include full match as first item checkbox + "\t\t\t\t\"numCols\": {\"type\": \"array\", \"items\": {\"type\": \"integer\"}, \"minItems\": 1}\r\n" + // same as first branch + "\t\t\t}\r\n" + + "\t\t}\r\n" + + "\t]\r\n" + + "}") + ); } public void GrabFocus() { + Show(); RegexTextBox.Focus(); } - public void SearchButton_Click(object sender, EventArgs e) + /// + /// if a treeview is open for the current document, do nothing

+ /// Otherwise, show a message box warning of no tree view (unless that message box has already been shown) + /// then open a treeview for the current document + ///
+ /// + private void WarnIfNoTreeView() { - regex = RegexTextBox.Text; - if (regex.IndexOf('(') < 0) - { - MessageBox.Show("Regex must have at least one capture group", "No capture groups", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - int regexLen = Encoding.UTF8.GetByteCount(regex); - FindOption findOption = FindOption.REGEXP; - if (!IgnoreCaseCheckBox.Checked) - findOption |= FindOption.MATCHCASE; - Npp.editor.SetSearchFlags(findOption); - var startEnds = SelectionManager.GetSelectedRanges(); - (int searchStart, int searchEnd) = startEnds[0]; - if (startEnds.Count == 1 && searchEnd > searchStart) + if (Main.openTreeViewer is null || Main.openTreeViewer.IsDisposed || Main.openTreeViewer.fname != Npp.notepad.GetCurrentFilePath()) { - // for simplicity, only set target to selection for nonzero length single selection - Npp.editor.TargetFromSelection(); + Main.OpenJsonTree(DocumentType.REGEX); } - else - { - Npp.editor.TargetWholeDocument(); - } - int[] groupsToParseAsNumber; - if (NumGroupsTextBox.Text.Length == 0) - { - groupsToParseAsNumber = new int[0]; - } - else - { - try - { - groupsToParseAsNumber = jsonParser.ParseArray(NumGroupsTextBox.Text, 0).children - .Select(x => (int)x.value) - .ToArray(); - if (!groupsToParseAsNumber.All(x => x > 0)) - throw new ArgumentException("All group numbers to be parsed as number must be greater than 0"); - } - catch (Exception ex) - { - MessageBox.Show($"While trying to parse \"Groups to parse as number\" box as integer array, got error {RemesParser.PrettifyException(ex)}", - "Could not parse \"groups to parse as number\" box", - MessageBoxButtons.OK, MessageBoxIcon.Error); - groupsToParseAsNumber = new int[0]; - } - } - var allMatches = new List(); - int numTags = int.MaxValue; - int indexInGroupsToParseAsNumber = 0; - while (true) + } + + private static readonly string[] EOLS = new[] { "`\\r\\n`", "`\\n`", "`\\r`" }; + + private const string HEADER_HANDLING_ABBREVS = "nhd"; + + private static readonly Dictionary HEADER_HANDLING_ABBREV_MAP = new Dictionary { ["\"h\""] = 1, ["\"n\""] = 0, ["\"d\""] = 2, ["1"] = 1, ["2"] = 2, ["0"] = 0 }; + + private static readonly Dictionary NEWLINE_MAP = new Dictionary { ["\"\\r\\n\""] = 0, ["\"\\n\""] = 1, ["\"\\r\""] = 2, ["0"] = 0, ["1"] = 1, ["2"] = 2 }; + + public void SearchButton_Click(object sender, EventArgs e) + { + WarnIfNoTreeView(); + string columnsToParseAsNumber = ""; + if (ColumnsToParseAsNumberTextBox.Text.Length > 0) { - int nextMatchPos = Npp.editor.SearchInTarget(regexLen, regex); - if (nextMatchPos < 0) - break; - var tagJsons = new List(); - for (int currentTag = 1; currentTag < numTags; currentTag++) - { - string tagText = Npp.editor.GetTag(currentTag); - // TODO: How to deal with actual empty capture groups (as opposed to a dummy after the last capture group)? - // PythonScript somehow has direct access to the Boost regex match objects; how can we access those? - if (tagText.Length == 0 && numTags == int.MaxValue) - break; - JNode tagJson; - if (indexInGroupsToParseAsNumber < groupsToParseAsNumber.Length && groupsToParseAsNumber[indexInGroupsToParseAsNumber] == currentTag) - { - tagJson = ParseNumber(tagText, nextMatchPos); - indexInGroupsToParseAsNumber++; - } - else - tagJson = new JNode(tagText, nextMatchPos); - tagJsons.Add(tagJson); - } - if (tagJsons.Count > 0) - allMatches.Add(new JArray(nextMatchPos, tagJsons)); + JNode columnsToParseAsNumberArr = jsonParser.Parse(ColumnsToParseAsNumberTextBox.Text); + if (jsonParser.fatal || !(columnsToParseAsNumberArr is JArray arr) || arr.Length == 0 || arr.children.Any(x => x.type != Dtype.INT)) + MessageBox.Show("Columns to parse as number must be a nonempty JSON array of integers", "Columns to parse as number must be int array", MessageBoxButtons.OK, MessageBoxIcon.Error); + else + columnsToParseAsNumber = ", " + columnsToParseAsNumberArr.ToString(false).Slice("1:-1"); } - if (allMatches.Count > 0) + if (ParseAsCsvCheckBox.Checked) { - var newJson = new JArray(searchStart, allMatches); - Main.AddJsonForFile(Npp.notepad.GetCurrentFilePath(), newJson); + string delim = DelimiterTextBox.Text == "\\t" ? "`\\t`" : RemesPathLexer.StringToBacktickString(DelimiterTextBox.Text); + string quote = RemesPathLexer.StringToBacktickString(QuoteCharTextBox.Text); + string newline = EOLS[NewlineComboBox.SelectedIndex]; + char headerHandlingAbbrev = HEADER_HANDLING_ABBREVS[HeaderHandlingComboBox.SelectedIndex]; + Main.openTreeViewer.QueryBox.Text = $"s_csv(@, {NColumnsTextBox.Text}, {delim}, {newline}, {quote}, {headerHandlingAbbrev}{columnsToParseAsNumber})"; } else { - MessageBox.Show("No matches, or no capture groups", "Search failed", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; + string ignoreCaseFlag = IgnoreCaseCheckBox.Checked ? "(?i)" : "(?-i)"; + string regex = RemesPathLexer.StringToBacktickString(ignoreCaseFlag + RegexTextBox.Text); + string includeFullMatchAsFirstItem = IncludeFullMatchAsFirstItemCheckBox.Checked ? "true" : "false"; + Main.openTreeViewer.QueryBox.Text = $"s_fa(@, {regex}, {includeFullMatchAsFirstItem}{columnsToParseAsNumber})"; } + if (!Main.openTreeViewer.Visible) + Npp.notepad.ShowDockingForm(Main.openTreeViewer); + Main.openTreeViewer.SubmitQueryButton.PerformClick(); } + public void ParseAsCsvCheckBox_CheckedChanged(object sender, EventArgs e) + { + bool showCsvButtons = ParseAsCsvCheckBox.Checked; + QuoteCharTextBox.Visible = showCsvButtons; + QuoteCharTextBoxLabel.Visible = showCsvButtons; + DelimiterTextBox.Visible = showCsvButtons; + DelimiterTextBoxLabel.Visible = showCsvButtons; + NewlineComboBox.Visible = showCsvButtons; + NewlineComboBoxLabel.Visible = showCsvButtons; + HeaderHandlingComboBox.Visible = showCsvButtons; + HeaderHandlingComboBoxLabel.Visible = showCsvButtons; + NColumnsTextBox.Visible = showCsvButtons; + NColumnsTextBoxLabel.Visible = showCsvButtons; + RegexTextBox.Enabled = !showCsvButtons; + IgnoreCaseCheckBox.Enabled = !showCsvButtons; + IncludeFullMatchAsFirstItemCheckBox.Enabled = !showCsvButtons; + } + + /// - /// try to parse inp as a number. If inp can't be parsed as a number, return a JNode with inp as value.

- /// The following number formats (and corresponding types) can be parsed (leading + and - signs are both allowed for all)

- /// integers (base 10 or hex) => integer

- /// decimal numbers (leading or trailing '.' both allowed, as is scientific notation)

- /// NaN -> NaN

- /// Infinity -> Infinity + /// settings must be an object following the JSON schema shown in the constructor for this class

+ /// if settings does not follow that schema, return false and errorMessage = the validation problem

+ /// if settings follows the scheam, set settings according to the object and return true ///
- /// - /// - /// - public JNode ParseNumber(string inp, int pos) + /// a JSON object representing the settings to use + /// was this invoked by a user (if true, show a message box if there's an issue) + public bool SetFieldsFromJson(JNode settingsNode, bool wasCalledByUser, out string errorMessage) { - // parsed tracks which portions of a number have been parsed. - // So if the int part has been parsed, it will be 1. - // If the int and decimal point parts have been parsed, it will be 3. - // If the int, decimal point, and scientific notation parts have been parsed, it will be 7 - int parsed = 1; - char c = inp[0]; - bool negative = false; - int ii = 0; - if (c < '0' || c > '9') - { - if (c == '-' || c == '+') - { - if (c == '-') - negative = true; - ii++; - } - c = inp[ii]; - // Infinity - if (c == 'I' && ii <= inp.Length - 8 && inp[ii + 1] == 'n' && inp.Substring(ii + 2, 6) == "finity") - { - double infty = negative ? NanInf.neginf : NanInf.inf; - return new JNode(infty, pos); - } - // NaN - else if (c == 'N' && ii <= inp.Length - 3 && inp[ii + 1] == 'a' && inp[ii + 2] == 'N') - { - return new JNode(NanInf.nan, pos); - } - else if (c < '0' || c > '9') - return new JNode(inp, pos); - } - if (c == '0' && ii < inp.Length - 1 && inp[ii + 1] == 'x') - { - ii += 2; - int start = ii; - while (ii < inp.Length) - { - c = inp[ii]; - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) - break; - ii++; - } - var hexnum = long.Parse(inp.Substring(start, ii - start), NumberStyles.HexNumber); - return new JNode(negative ? -hexnum : hexnum, pos); - } - while (ii < inp.Length) + var vp = settingsValidator(settingsNode); + errorMessage = null; + if (vp != null) { - c = inp[ii]; - if (c >= '0' && c <= '9') - { - ii++; - } - else if (c == '.') - { - if (parsed != 1) - { - return new JNode(inp, pos); // too many decimal points - } - parsed = 3; - ii++; - } - else if (c == 'e' || c == 'E') - { - if ((parsed & 4) != 0) - { - return new JNode(inp, pos); // already saw scientific notation 'e' - } - parsed += 4; - ii++; - if (ii < inp.Length) - { - c = inp[ii]; - if (c == '+' || c == '-') - { - ii++; - } - } - else - { - // Scientific notation 'e' with no number following - return new JNode(inp, pos); - } - } - else // not a number character, so just treat whole string as not a number - return new JNode(inp, pos); - } - if (parsed == 1) - { - try - { - return new JNode(long.Parse(inp), pos); - } - catch (OverflowException) - { - // doubles can represent much larger numbers than 64-bit ints, - // albeit with loss of precision - } + errorMessage = $"invalid settings {settingsNode.ToString()}, got validation problem {vp}"; + if (wasCalledByUser) + MessageBox.Show(errorMessage, + "invalid settings", + MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; } - double num; - try + JObject settings = (JObject)settingsNode; + ParseAsCsvCheckBox.Checked = (bool)settings["csv"].value; + ColumnsToParseAsNumberTextBox.Text = settings.children.TryGetValue("numCols", out JNode numColNode) && numColNode is JArray numColArr + ? numColArr.ToString() + : ""; + if (ParseAsCsvCheckBox.Checked) { - num = double.Parse(inp, JNode.DOT_DECIMAL_SEP); + DelimiterTextBox.Text = (string)settings["delim"].value; + QuoteCharTextBox.Text = (string)settings["quote"].value; + NColumnsTextBox.Text = settings["nColumns"].ToString(); + HeaderHandlingComboBox.SelectedIndex = HEADER_HANDLING_ABBREV_MAP[settings["header"].ToString()]; + NewlineComboBox.SelectedIndex = NEWLINE_MAP[settings["newline"].ToString()]; } - catch + else { - num = NanInf.nan; + IgnoreCaseCheckBox.Checked = (bool)settings["ignoreCase"].value; + IncludeFullMatchAsFirstItemCheckBox.Checked = (bool)settings["fullMatch"].value; + RegexTextBox.Text = (string)settings["regex"].value; } - return new JNode(num, pos); + return true; } } } diff --git a/JsonToolsNppPlugin/Forms/TreeViewer.cs b/JsonToolsNppPlugin/Forms/TreeViewer.cs index 6ef89f5..381dd81 100644 --- a/JsonToolsNppPlugin/Forms/TreeViewer.cs +++ b/JsonToolsNppPlugin/Forms/TreeViewer.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; using System.Security.Cryptography; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; @@ -57,6 +58,12 @@ public partial class TreeViewer : Form /// public bool shouldRefresh; + /// the most recently used delimiter character for s_csv in a RemesPath query + public char csvDelim; + + /// the most recently used quote character for s_csv in a RemesPath query + public char csvQuote; + // event handlers for the node mouseclick drop down menu private static MouseEventHandler valToClipboardHandler = null; private static MouseEventHandler pathToClipboardHandler_Remespath = null; @@ -81,6 +88,8 @@ public TreeViewer(JNode json) remesParser = new RemesParser(); lexer = new RemesPathLexer(); findReplaceForm = null; + csvDelim = '\x00'; + csvQuote = '\x00'; FormStyle.ApplyStyle(this, Main.settings.use_npp_styling); } @@ -397,6 +406,7 @@ void RuntimeErrorMessage(Exception ex, string selectionStartEnd = null) // modified JSON after the query has been executed if (queryFunc.IsMutator) { + // JMutators always return the input, but a multistep query that mutates the input could return something else bool isMultiStepQuery = queryFunc is JQueryContext; if (usesSelections) { @@ -459,6 +469,8 @@ void RuntimeErrorMessage(Exception ex, string selectionStartEnd = null) } else if (documentType == DocumentType.JSONL && queryFunc is JArray arr) formatter = Main.ToJsonLinesFromSettings; + else if (documentType == DocumentType.REGEX) + formatter = (JNode x) => x.ValueOrToString(); Dictionary keyChanges = Main.ReformatFileWithJson(queryFunc, formatter, usesSelections); if (isMultiStepQuery && usesSelections && treeFunc is JObject treeObj_) { @@ -517,6 +529,8 @@ void RuntimeErrorMessage(Exception ex, string selectionStartEnd = null) queryResult = queryFunc; treeFunc = queryResult; } + csvDelim = ArgFunction.csvDelimiterInLastQuery; + csvQuote = ArgFunction.csvQuoteCharInLastQuery; JsonTreePopulate(treeFunc); } @@ -994,10 +1008,13 @@ public void SelectTreeNodeJson(TreeNode node) if (Main.activeFname != fname) return; int nodeStartPos = 0, nodeEndPos = 0; - if (pathsToJNodes.TryGetValue(node.FullPath, out _)) + if (pathsToJNodes.TryGetValue(node.FullPath, out JNode jnode)) { nodeStartPos = NodePosInJsonDoc(node); - nodeEndPos = Main.EndOfJNodeAtPos(nodeStartPos, Npp.editor.GetLength()); + if (GetDocumentType() == DocumentType.REGEX) + nodeEndPos = nodeStartPos + JsonParser.UTF8BytesInCSVRepr(jnode.ValueOrToString(), csvDelim, csvQuote); + else + nodeEndPos = Main.EndOfJNodeAtPos(nodeStartPos, Npp.editor.GetLength()); } if (nodeStartPos == nodeEndPos) MessageBox.Show("The selected tree node does not appear to correspond to a JSON element in the document.", @@ -1018,6 +1035,35 @@ public void SelectTreeNodeJsonChildren(TreeNode node) MessageBox.Show("The selected tree node does not appear to correspond to a JSON element in the document.", "Couldn't select children of JSON", MessageBoxButtons.OK, MessageBoxIcon.Error); int selectionStartPos = ParentSelectionStartPos(node); + if (GetDocumentType() == DocumentType.REGEX) + { + bool firstSelectionSet = false; + IEnumerable children = (jnode is JArray arr_) ? arr_.children : ((JObject)jnode).children.Values.AsEnumerable(); + Npp.editor.ClearSelections(); + foreach (JNode child in children) + { + if (child is JArray || child is JObject) + { + MessageBox.Show("Cannot select an object or an array in a non-JSON document, as it does not correspond to a specific text region", + "Can't select object or array in non-JSON", + MessageBoxButtons.OK, MessageBoxIcon.Error); + break; + } + string childstr = child.ValueOrToString(); + int utf8Len = JsonParser.UTF8BytesInCSVRepr(childstr, csvDelim, csvQuote); + int startPos = child.position; + int endPos = startPos + utf8Len; + if (endPos > startPos) + { + if (firstSelectionSet) + Npp.editor.AddSelection(startPos, endPos); + else + Npp.editor.SetSelection(startPos, endPos); + firstSelectionSet = true; + } + } + return; + } IEnumerable positions; if (jnode is JArray arr) positions = arr.children.Select(x => selectionStartPos + x.position); diff --git a/JsonToolsNppPlugin/JSONTools/JNode.cs b/JsonToolsNppPlugin/JSONTools/JNode.cs index 50497c9..b8f2374 100644 --- a/JsonToolsNppPlugin/JSONTools/JNode.cs +++ b/JsonToolsNppPlugin/JSONTools/JNode.cs @@ -178,10 +178,8 @@ public enum DocumentType JSONL, /// ini files INI, - /// regex search results + /// regex search results (includes CSV files parsed with s_csv) REGEX, - /// csv files (differs from REGEX only in handling of quoted values) - CSV, } /// @@ -448,6 +446,15 @@ public virtual string ToString(bool sort_keys = true, string key_value_sep = ": } } + /// + /// return this.value if this happens to have a string value, else this.ToString() + /// + /// + public string ValueOrToString() + { + return value is string s ? s : ToString(); + } + internal virtual int ToStringHelper(bool sort_keys, string key_value_sep, string item_sep, StringBuilder sb, bool change_positions, int extra_utf8_bytes, int max_length) { if (change_positions) @@ -2055,6 +2062,7 @@ private JNode EvaluateStatementsFromStartToEnd(JNode inp, int start, int end) { JNode lastStatement = null; indexInStatements = start; + ArgFunction.regexSearchResultsShouldBeCached = !mutatesInput; while (indexInStatements < end) { JNode statement = statements[indexInStatements]; @@ -2074,6 +2082,7 @@ private JNode EvaluateStatementsFromStartToEnd(JNode inp, int start, int end) indexInStatements++; } } + ArgFunction.regexSearchResultsShouldBeCached = true; return lastStatement; } diff --git a/JsonToolsNppPlugin/JSONTools/JsonParser.cs b/JsonToolsNppPlugin/JSONTools/JsonParser.cs index 839873c..710186f 100644 --- a/JsonToolsNppPlugin/JSONTools/JsonParser.cs +++ b/JsonToolsNppPlugin/JSONTools/JsonParser.cs @@ -311,6 +311,10 @@ public static int ExtraUTF8Bytes(char c) : 1; // non-ascii chars less than 2048 take up 2 bytes } + /// + /// gets the number of extra bytes (greater than end - start) in inp + /// beteeen 0-based index start (inclusive) and end (exclusive) + /// public static int ExtraUTF8BytesBetween(string inp, int start, int end) { int count = 0; @@ -321,6 +325,36 @@ public static int ExtraUTF8BytesBetween(string inp, int start, int end) return count; } + /// + /// number of bytes in the UTF8-encoded CSV representation of string s (with delimiter delim and quote character quote)

+ /// e.g. "fö\n'" would be represented as 'fö\n''' in a CSV file with '\'' as quote character, so UTF8BytesInCSVRepr("fö\n'", ',', '\'') would return 8.

+ /// if delim is '\x00', return the UTF8 bytecount of s + ///
+ /// CSV delimiter (or '\x00' for non-CSV file) + /// CSV quote character + /// + public static int UTF8BytesInCSVRepr(string s, char delim, char quote) + { + if (delim == 0) + return Encoding.UTF8.GetByteCount(s); + int quoteCount = 0; + bool delimOrNewline = false; + int byteCount = s.Length; + for (int ii = 0; ii < s.Length; ii++) + { + char c = s[ii]; + if (c == '\r' || c == '\n' || c == delim) + delimOrNewline = true; + else if (c == quote) + quoteCount++; + else + byteCount += ExtraUTF8Bytes(c); + } + if (delimOrNewline || quoteCount > 0) + byteCount += 2 + quoteCount; // strings containing newlines, quotes or delimiters must be wrapped in quotes, and quote chars in quoted strings must be doubled up + return byteCount; + } + /// /// Set the parser's state to severity, unless the state was already higher.

/// If the severity is above the parser's loggerLevel:

diff --git a/JsonToolsNppPlugin/JSONTools/JsonSchemaValidator.cs b/JsonToolsNppPlugin/JSONTools/JsonSchemaValidator.cs index c7b152c..a0f0a4f 100644 --- a/JsonToolsNppPlugin/JSONTools/JsonSchemaValidator.cs +++ b/JsonToolsNppPlugin/JSONTools/JsonSchemaValidator.cs @@ -133,12 +133,14 @@ public override string ToString() } } + public delegate ValidationProblem? ValidationFunc(JNode x); + private struct RegexAndValidator { public Regex regex; - public Func validator; + public ValidationFunc validator; - public RegexAndValidator(Regex regex, Func validator) + public RegexAndValidator(Regex regex, ValidationFunc validator) { this.regex = regex; this.validator = validator; @@ -160,7 +162,7 @@ public static bool TypeValidates(Dtype json_type, Dtype schema_type) ///
/// a JNode representing a parsed JSON schema /// - public static Func CompileValidationFunc(JNode schema_) + public static ValidationFunc CompileValidationFunc(JNode schema_) { if (!(schema_ is JObject obj) || !( obj.children.TryGetValue("definitions", out JNode defs) @@ -180,7 +182,7 @@ public static bool TypeValidates(Dtype json_type, Dtype schema_type) /// /// /// - public static Func CompileValidationFuncHelper(JNode schema_, JObject definitions, int recursions) + public static ValidationFunc CompileValidationFuncHelper(JNode schema_, JObject definitions, int recursions) { if (recursions == RECURSION_LIMIT) return (x) => new ValidationProblem(ValidationProblemType.RECURSION_LIMIT_REACHED, new Dictionary { }, 0); @@ -195,7 +197,7 @@ public static bool TypeValidates(Dtype json_type, Dtype schema_type) return (x) => null; // the empty schema validates everything if (schema.children.TryGetValue("$ref", out JNode refnode)) { - // a $ref should go to a "valid schema location path" + // a $ref should go to a "valid schema location path // which would look something like "#/$defs/foo" // or "#/definitions/bar". // there's a lot of detail in the JSON schema specification @@ -212,12 +214,36 @@ public static bool TypeValidates(Dtype json_type, Dtype schema_type) var func = CompileValidationFuncHelper(def, definitions, recursions); return func; } + if (schema.children.TryGetValue("enum", out JNode enum_) && enum_ is JArray enumarr) + { + // the "enum" keyword means that the JSON must have + // one of the values in the associated array. + // since the enumeration array implicitly defines the allowed types, we don't need to specify a "type" keyword + var enumMembers = enumarr.children; + return (json) => + { + foreach (JNode possible in enumMembers) + { + if (possible.type != json.type) continue; + if (possible.Equals(json)) return null; + } + return new ValidationProblem( + ValidationProblemType.VALUE_NOT_IN_ENUM, + new Dictionary + { + { "found", json }, + { "enum", enumarr }, + }, + json.position + ); + }; + } if (!schema.children.TryGetValue("type", out JNode type)) { // an anyOf array of allowable schemas if (!schema.children.TryGetValue("anyOf", out JNode anyOf)) { - throw new SchemaValidationException("Each schema must have one of the '$ref', 'anyOf', or 'type' keywords."); + throw new SchemaValidationException("Each schema must have one of the '$ref', 'anyOf', 'type', or 'enum' keywords."); } var subValidators = ((JArray)anyOf).children .Select((subschema) => CompileValidationFuncHelper(subschema, definitions, recursions)) @@ -261,31 +287,8 @@ public static bool TypeValidates(Dtype json_type, Dtype schema_type) ); }; } - var dtype = JsonSchemaMaker.typeNameToDtype[(string)type.value]; // now do any additional validation as needed - if (schema.children.TryGetValue("enum", out JNode enum_) && enum_ is JArray enumarr) - { - // the "enum" keyword means that the JSON must have - // one of the values in the associated array - var enumMembers = enumarr.children; - return (json) => - { - foreach (JNode possible in enumMembers) - { - if (possible.type != json.type) continue; - if (possible.Equals(json)) return null; - } - return new ValidationProblem( - ValidationProblemType.VALUE_NOT_IN_ENUM, - new Dictionary - { - { "found", json }, - { "enum", enumarr }, - }, - json.position - ); - }; - } + var dtype = JsonSchemaMaker.typeNameToDtype[(string)type.value]; // validation logic for arrays if (dtype == Dtype.ARR) { @@ -429,7 +432,7 @@ public static bool TypeValidates(Dtype json_type, Dtype schema_type) // validation logic for objects if (dtype == Dtype.OBJ) { - var propsValidators = new Dictionary>(); + var propsValidators = new Dictionary(); if (schema.children.TryGetValue("properties", out JNode properties)) { // standard validation: one schema per key in "properties" diff --git a/JsonToolsNppPlugin/JSONTools/JsonTabularize.cs b/JsonToolsNppPlugin/JSONTools/JsonTabularize.cs index 38f8feb..c762cf2 100644 --- a/JsonToolsNppPlugin/JSONTools/JsonTabularize.cs +++ b/JsonToolsNppPlugin/JSONTools/JsonTabularize.cs @@ -804,7 +804,7 @@ public JArray BuildTable(JNode obj, Dictionary schema, string ke /// /// /// - private void ApplyQuotesIfNeeded(StringBuilder sb, string s, char delim, char quote_char) + public static void ApplyQuotesIfNeeded(StringBuilder sb, string s, char delim, char quote_char) { if (s.IndexOfAny(new char[] {delim, '\r', '\n', quote_char}) >= 0) { diff --git a/JsonToolsNppPlugin/JSONTools/RemesPath.cs b/JsonToolsNppPlugin/JSONTools/RemesPath.cs index 1daf659..575c995 100644 --- a/JsonToolsNppPlugin/JSONTools/RemesPath.cs +++ b/JsonToolsNppPlugin/JSONTools/RemesPath.cs @@ -1198,6 +1198,8 @@ private JNode ParseQuery(List toks) int pos = 0; int end = toks.Count; var context = new JQueryContext(); + ArgFunction.csvDelimiterInLastQuery = '\x00'; + ArgFunction.csvQuoteCharInLastQuery = '\x00'; Dictionary isVarnameFunctionOfInput = DetermineWhichVariablesAreFunctionsOfInput(toks); while (pos < end) { @@ -1241,9 +1243,10 @@ private Dictionary DetermineWhichVariablesAreFunctionsOfInput(List else pos = endOfStatement + 1; } + ArgFunction.regexSearchResultsShouldBeCached = !containsMutation; // any mutation could potentially mutate values inside the cache, which is very bad if (containsMutation) { - // no variable is safe from the mutation, not even ones that were declared before the mutation expression (thanks, for loops!) + // no variable is safe from the mutation, not even ones that were declared before the mutation expression foreach (string varname in isVarnameFunctionOfInput.Keys.ToArray()) isVarnameFunctionOfInput[varname] = true; } @@ -2281,6 +2284,10 @@ public static string PrettifyException(Exception ex) { return rpioore.ToString(); } + if (ex is SchemaValidationException sve) + { + return sve.ToString(); + } if (ex is DsonDumpException dde) { return $"DSON dump error: {dde.Message}"; diff --git a/JsonToolsNppPlugin/JSONTools/RemesPathFunctions.cs b/JsonToolsNppPlugin/JSONTools/RemesPathFunctions.cs index 44f4c63..9f05ca7 100644 --- a/JsonToolsNppPlugin/JSONTools/RemesPathFunctions.cs +++ b/JsonToolsNppPlugin/JSONTools/RemesPathFunctions.cs @@ -2336,15 +2336,45 @@ public static JNode StrFind(List args) return new JArray(0, result_list); } - //public const int MAX_DOC_SIZE_CACHE_REGEX_SEARCH = 10_000_000; + /// + /// any document smaller than this isn't worth caching results for + /// + public const int MIN_DOC_SIZE_CACHE_REGEX_SEARCH = 100_000; + + /// + /// any input larger than this (5 megabytes for 32bit, 10 megabytes for 64bit) will not have s_csv or s_fa outputs cached, to avoid eating too much memory + /// + private static readonly int MAX_DOC_SIZE_CACHE_REGEX_SEARCH = IntPtr.Size * 1_250_000; + + /// + /// do not cache for any reason. This is usually because the query involves mutation and there is some danger of mutating a value in the cache. + /// + public static bool regexSearchResultsShouldBeCached = true; + + private const int regexSearchResultCacheSize = 8; - // parses a single CSV value (one column in one row) according to RFC 4180 (https://www.ietf.org/rfc/rfc4180.txt) + /// + /// caches the results of executing s_fa or s_csv with a specific set of arguments on a specific input.

+ /// Only used if regexSearchResultsShouldBeCached and the size of the input is between MIN_DOC_SIZE_CACHE_REGEX_SEARCH and MAX_DOC_SIZE_CACHE_REGEX_SEARCH. + ///
+ private static LruCache<(string input, string argsAsJArrayString), JNode> regexSearchResultCache = new LruCache<(string input, string argsAsJArrayString), JNode>(regexSearchResultCacheSize); + + public static char csvDelimiterInLastQuery = '\x00'; + public static char csvQuoteCharInLastQuery = '\x00'; + + /// + /// parses a single CSV value (one column in one row) according to RFC 4180 (https://www.ietf.org/rfc/rfc4180.txt) + /// public const string CSV_BASE_COLUMN_REGEX = "([^{DELIM}\\r\\n{QUOTE}]*|{QUOTE}(?:[^{QUOTE}]|{QUOTE}{2})*{QUOTE})"; - // captures an integer (0x or normal decimal) with leading + or - sign + /// + /// captures an integer (0x or normal decimal) with leading + or - sign + /// public const string CAPTURED_INT_REGEX_STR = "([+-]?(?:0x[\\da-fA-F]+|\\d+))"; - // captures a floating point number. Scientific notation is allowed, as are trailing or leading decimal points + /// + /// captures a floating point number. Scientific notation is allowed, as are trailing or leading decimal points + /// public const string CAPTURED_FLOAT_REGEX_STR = "([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)"; // variants of the above two, but they don't capture @@ -2411,7 +2441,7 @@ public static string CsvRowRegex(int nColumns, char delimiter=',', string newlin /// /// `csv_regex(nColumns: int, delim: string=",", newline: string="\r\n", quote_char: string="\"")` - /// Returns the regex that s_csv (see ReadCsv function below) uses to match a single row of a CSV file + /// Returns the regex that s_csv (see CsvRead function below) uses to match a single row of a CSV file /// with delimiter `delim`, `nColumns` columns, quote character `quote_char`, and newline `newline`. /// /// @@ -2567,6 +2597,47 @@ public static IEnumerable EnumerateGroupsOfRegexMatch(string text, int ma } } + private static bool UseRegexSearchResultCache(string input) + { + return regexSearchResultsShouldBeCached && input.Length >= MIN_DOC_SIZE_CACHE_REGEX_SEARCH && input.Length <= MAX_DOC_SIZE_CACHE_REGEX_SEARCH; + } + + private static string ArgsAsJArrayString(Regex rex, HeaderHandlingInCsv headerHandling, int[] columnsToParseAsNumber) + { + var sb = new StringBuilder("["); + sb.Append(JNode.StrToString(rex.ToString(), true)); + sb.Append(",\""); + sb.Append(headerHandling.ToString()); + sb.Append("\","); + for (int ii = 0; ii < columnsToParseAsNumber.Length; ii++) + { + sb.Append(columnsToParseAsNumber[ii]); + sb.Append(ii == columnsToParseAsNumber.Length - 1 ? ']' : ','); + } + return sb.ToString(); + } + + /// + /// if the regexSearchResultCache is usable for this input at this time, check if this combination of (input, regex, HeaderHandlingInCsv, columnsToParseAsNumber) + /// is in the regexSearchResultCache and return the cached value if so. + /// + private static bool TryGetCachedRegexSearchResults(string input, Regex rex, HeaderHandlingInCsv headerHandling, int[] columnsToParseAsNumber, out JNode cachedOutput) + { + cachedOutput = null; + if (!UseRegexSearchResultCache(input)) + return false; + string argsAsJArrayString = ArgsAsJArrayString(rex, headerHandling, columnsToParseAsNumber); + return regexSearchResultCache.TryGetValue((input, argsAsJArrayString), out cachedOutput); + } + + private static void CacheResultsOfRegexSearch(string input, Regex rex, HeaderHandlingInCsv headerHandling, int[] columnsToParseAsNumber, JNode output) + { + if (!UseRegexSearchResultCache(input)) + return; + string argsAsJArrayString = ArgsAsJArrayString(rex, headerHandling, columnsToParseAsNumber); + regexSearchResultCache[(input, argsAsJArrayString)] = output; + } + /// /// return an array of strings(if rex has 0 or 1 capture group(s)) or an array of arrays of strings (if there are multiple capture groups)

/// The arguments in args at index firstOptionalArgNum onward will be treated as the 0-based indices of capture groups to parse as numbers

@@ -2605,11 +2676,12 @@ JNode matchEvaluator(string mValue, bool tryParseAsNumber, int jnodePosition) return parsed; } if (csvQuoteChar > 0 && mValue.Length > 0 && mValue[0] == csvQuoteChar) - return new JNode(mValue.Substring(1, mValue.Length - 2).Replace(doubleQuoteStr, quoteStr)); - return new JNode(mValue); + return new JNode(mValue.Substring(1, mValue.Length - 2).Replace(doubleQuoteStr, quoteStr), jnodePosition); + return new JNode(mValue, jnodePosition); } int minGroupNum = headerHandling == HeaderHandlingInCsv.INCLUDE_FULL_MATCH_AS_FIRST_ITEM ? 0 : 1; int nColumns = minGroupNum >= maxGroupNum ? 1 : maxGroupNum + 1 - minGroupNum; + JNode output; while (nextMatchStart < text.Length) { Match m = rex.Match(text, nextMatchStart); @@ -2618,7 +2690,7 @@ JNode matchEvaluator(string mValue, bool tryParseAsNumber, int jnodePosition) int matchStart = m.Index; utf8ExtraBytes += JsonParser.ExtraUTF8BytesBetween(text, matchEnd, matchStart); int jnodePos = matchStart + utf8ExtraBytes; - matchEnd = m.Index + m.Length; + matchEnd = matchStart + m.Length; if (maxGroupNum < 0) { maxGroupNum = m.Groups.Count - 1; @@ -2632,11 +2704,29 @@ JNode matchEvaluator(string mValue, bool tryParseAsNumber, int jnodePosition) } Array.Sort(columnsToParseAsNumber); } + if (isFirstRow && TryGetCachedRegexSearchResults(text, rex, headerHandling, columnsToParseAsNumber, out output)) + return output; bool parseMatchesAsRow = headerHandling == HeaderHandlingInCsv.INCLUDE_HEADER_ROWS_AS_ARRAYS || headerHandling == HeaderHandlingInCsv.INCLUDE_FULL_MATCH_AS_FIRST_ITEM || !isFirstRow; if (nColumns == 1) { int nextColToParseAsNumber = columnsToParseAsNumber.Length == 0 ? -1 : columnsToParseAsNumber[0]; - string mValue = maxGroupNum == 1 ? m.Groups[1].Value : m.Value; + string mValue; + int childPos; + if (maxGroupNum == 1) + { + Group grp1 = m.Groups[1]; + int grpIndex = grp1.Index; + utf8ExtraBytes += JsonParser.ExtraUTF8BytesBetween(text, matchStart, grpIndex); + childPos = grpIndex + utf8ExtraBytes; + mValue = grp1.Value; + utf8ExtraBytes += JsonParser.ExtraUTF8BytesBetween(text, grpIndex, matchEnd); + } + else + { + mValue = m.Value; + childPos = jnodePos; + utf8ExtraBytes += JsonParser.ExtraUTF8BytesBetween(text, matchStart, matchEnd); + } if (isFirstRow) { if (headerHandling == HeaderHandlingInCsv.MAP_HEADER_TO_ROWS) @@ -2646,7 +2736,7 @@ JNode matchEvaluator(string mValue, bool tryParseAsNumber, int jnodePosition) } if (parseMatchesAsRow) { - JNode nodeToAdd = matchEvaluator(mValue, nextColToParseAsNumber >= 0, jnodePos); + JNode nodeToAdd = matchEvaluator(mValue, nextColToParseAsNumber >= 0, childPos); if (headerHandling == HeaderHandlingInCsv.MAP_HEADER_TO_ROWS) { // if we have a 1-column file and we want to map the header to rows, we will map the one value @@ -2692,11 +2782,15 @@ JNode matchEvaluator(string mValue, bool tryParseAsNumber, int jnodePosition) rows.Add(new JArray(jnodePos, row)); } } + else // still need to get utf8 extra bytes in the first row + utf8ExtraBytes += JsonParser.ExtraUTF8BytesBetween(text, matchStart, matchEnd); } nextMatchStart = matchEnd > matchStart ? matchEnd : matchStart + 1; isFirstRow = false; } - return new JArray(0, rows); + output = new JArray(0, rows); + CacheResultsOfRegexSearch(text, rex, headerHandling, columnsToParseAsNumber, output); + return output; } /// @@ -2721,8 +2815,10 @@ public static JNode CsvRead(List args) throw new RemesPathArgumentException($"second arg (nColumns) to s_csv must be integer, not {args[1].type}", 1, FUNCTIONS["s_csv"]); int nColumns = Convert.ToInt32(args[1].value); char delim = args.Count > 2 ? ((string)args[2].value)[0] : ','; + csvDelimiterInLastQuery = delim; string newline = args.Count > 3 ? (string)args[3].value : "\r\n"; char quote = args.Count > 4 ? ((string)args[4].value)[0] : '"'; + csvQuoteCharInLastQuery = quote; string headerHandlingAbbrev = args.Count > 5 ? (string)args[5].value : "n"; if (!HEADER_HANDLING_ABBREVIATIONS.TryGetValue(headerHandlingAbbrev, out HeaderHandlingInCsv headerHandling)) throw new RemesPathArgumentException("header_handling (6th argument, default 'n') must be 'n' (no header, rows as arrays), 'h' (include header, rows as arrays), or 'd' (rows as objects with header as keys)", 5, FUNCTIONS["s_csv"]); @@ -2730,6 +2826,85 @@ public static JNode CsvRead(List args) return (JArray)StrFindAllHelper(text, new Regex(rexPat, RegexOptions.Compiled), args, 6, "s_csv", nColumns, headerHandling, quote); } + /// + /// to_csv(x: array, delimiter: string=",", newline: string="\r\n", quote_char: string="\"") -> string

+ /// returns x formatted as a CSV (RFC 4180 rules as normal), according to the following rules:

+ /// * if x is an array of non-iterables, each child is converted to a string on a separate line

+ /// * if x is an array of arrays, each subarray is converted to a row

+ /// * if x is an array of objects, the keys of the first subobject are converted to a header row, and the values of every subobject become their own row. + ///
+ /// + /// + public static JNode ToCsv(List args) + { + JArray arr = (JArray)args[0]; + if (arr.Length == 0) + return new JNode(""); + char delim = args[1].value is string s2 ? s2[0] : ','; + string newline = args[2].value is string s3 ? s3 : "\r\n"; + char quote = args[3].value is string s4 ? s4[0] : '"'; + var sb = new StringBuilder(); + JNode firstRow = arr[0]; + Func rowFormatter; + int nColumns = 0; + if (firstRow is JObject obj) + { + // treat keys as header + string[] keys = obj.children.Keys.ToArray(); + nColumns = keys.Length; + for (int ii = 0; ii < nColumns; ii++) + { + JsonTabularizer.ApplyQuotesIfNeeded(sb, JNode.UnescapedJsonString(keys[ii], false), delim, quote); + if (ii < keys.Length - 1) + sb.Append(delim); + } + sb.Append(newline); + rowFormatter = x => + { + JObject o = (JObject)x; + int ii = 0; + foreach (JNode ochild in o.children.Values) + { + JsonTabularizer.ApplyQuotesIfNeeded(sb, ochild.ValueOrToString(), delim, quote); + ii++; + if (ii < o.Length) + sb.Append(delim); + } + nColumns = ii; + sb.Append(newline); + return true; + }; + } + else + { + rowFormatter = x => + { + if (!(x is JArray a)) + { + JsonTabularizer.ApplyQuotesIfNeeded(sb, x.ValueOrToString(), delim, quote); + sb.Append(newline); + nColumns = 1; + return true; + } + nColumns = a.children.Count; + for (int ii = 0; ii < nColumns; ii++) + { + JsonTabularizer.ApplyQuotesIfNeeded(sb, a.children[ii].ValueOrToString(), delim, quote); + if (ii < a.Length - 1) + sb.Append(delim); + } + sb.Append(newline); + return true; + }; + } + foreach (JNode child in arr.children) + rowFormatter(child); + // include trailing newline unless it's a 1-column CSV (in which case the trailing newline would be interpreted as an empty final row) + if (nColumns == 1) + sb.Remove(sb.Length - newline.Length, newline.Length); + return new JNode(sb.ToString()); + } + /// /// s_fa(text: string, rex: regex | string, includeFullMatchAsFirstItem: bool = false, *colunsToParseAsNumber: int (optional))

/// If rex is a string, converts it to a regex

@@ -2855,11 +3030,6 @@ string replacementFunction(Match m) return new JNode(val.Replace(toReplaceStr, replStr)); } - public static JNode ToCsv(List args) - { - throw new NotImplementedException(); - } - /// /// returns true is x is string /// @@ -3139,6 +3309,7 @@ public static JNode ObjectsToJNode(object obj) ["sorted"] = new ArgFunction(Sorted, "sorted", Dtype.ARR, 1, 2, false, new Dtype[] {Dtype.ARR, Dtype.BOOL}), ["stringify"] = new ArgFunction(Stringify, "stringify", Dtype.STR, 1, 1, false, new Dtype[] { Dtype.ANYTHING }), ["sum"] = new ArgFunction(Sum, "sum", Dtype.FLOAT, 1, 1, false, new Dtype[] {Dtype.ARR}), + ["to_csv"] = new ArgFunction(ToCsv, "to_csv", Dtype.STR, 1, 4, false, new Dtype[] {Dtype.ARR, Dtype.STR, Dtype.STR, Dtype.STR}), ["to_records"] = new ArgFunction(ToRecords, "to_records", Dtype.ARR, 1, 2, false, new Dtype[] { Dtype.ITERABLE, Dtype.STR }), ["type"] = new ArgFunction(TypeOf, "type", Dtype.STR, 1, 1, false, new Dtype[] { Dtype.ANYTHING }), ["unique"] = new ArgFunction(Unique, "unique", Dtype.ARR, 1, 2, false, new Dtype[] {Dtype.ARR, Dtype.BOOL}), diff --git a/JsonToolsNppPlugin/JSONTools/RemesPathLexer.cs b/JsonToolsNppPlugin/JSONTools/RemesPathLexer.cs index 9b71df0..797e422 100644 --- a/JsonToolsNppPlugin/JSONTools/RemesPathLexer.cs +++ b/JsonToolsNppPlugin/JSONTools/RemesPathLexer.cs @@ -262,6 +262,9 @@ public static void BraceMatchCheck(string q, MatchCollection regtoks, List + /// RemesPath backtick string that will be parsed as string s + ///
public static string StringToBacktickString(string s) { var sb = new StringBuilder(); diff --git a/JsonToolsNppPlugin/Main.cs b/JsonToolsNppPlugin/Main.cs index 052e0e9..fc5c307 100644 --- a/JsonToolsNppPlugin/Main.cs +++ b/JsonToolsNppPlugin/Main.cs @@ -59,7 +59,7 @@ class Main private static string schemasToFnamePatternsFname = null; private static JObject schemasToFnamePatterns = new JObject(); private static SchemaCache schemaCache = new SchemaCache(16); - private static readonly Func schemasToFnamePatterns_SCHEMA = JsonSchemaValidator.CompileValidationFunc(new JsonParser().Parse("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\"," + + private static readonly JsonSchemaValidator.ValidationFunc schemasToFnamePatterns_SCHEMA = JsonSchemaValidator.CompileValidationFunc(new JsonParser().Parse("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\"," + "\"properties\":{},\"required\":[],\"type\":\"object\"," + // must be object "\"patternProperties\":{" + "\".+\":{\"items\":{\"type\":\"string\"},\"minItems\":1,\"type\":\"array\"}," + // nonzero-length keys must be mapped to non-empty string arrays @@ -401,6 +401,16 @@ static internal void PluginCleanUp() info.Dispose(); jsonFileInfos.Remove(key); } + if (sortForm != null && !sortForm.IsDisposed) + { + sortForm.Close(); + sortForm.Dispose(); + } + if (regexSearchForm != null && !regexSearchForm.IsDisposed) + { + regexSearchForm.Close(); + regexSearchForm.Dispose(); + } WriteSchemasToFnamePatternsFile(schemasToFnamePatterns); parseTimer.Dispose(); } @@ -500,9 +510,9 @@ public static (bool fatal, JNode node, bool usesSelections, DocumentType Documen if (wasAutotriggered && len > sizeThreshold) return (false, null, false, DocumentType.NONE); string text = Npp.editor.GetText(len + 1); - if (documentType == DocumentType.JSON || documentType == DocumentType.NONE) + if (documentType == DocumentType.JSON || documentType == DocumentType.NONE || // if user didn't specify how to parse the document + (documentType == DocumentType.REGEX && (info == null || info.json == null || info.json.type == Dtype.NULL))) // or user specified REGEX and document was not already parsed { - // if user didn't specify how to parse the document: // 1. check if the document has a file extension with an associated DocumentType string fileExtension = Npp.FileExtension().ToLower(); DocumentType docTypeFromExtension = Npp.DocumentTypeFromFileExtension(fileExtension); @@ -510,7 +520,7 @@ public static (bool fatal, JNode node, bool usesSelections, DocumentType Documen documentType = docTypeFromExtension; // 2. check if the user previously chose a document type, and use that if so. // this overrides the default for the file extension. - if (previouslyChosenDocType != DocumentType.NONE) + if (previouslyChosenDocType != DocumentType.NONE && documentType != DocumentType.REGEX) documentType = previouslyChosenDocType; } if (documentType == DocumentType.INI) @@ -532,6 +542,11 @@ public static (bool fatal, JNode node, bool usesSelections, DocumentType Documen fatalErrors.Add(errorMessage != null); comments = iniParser.comments; } + else if (documentType == DocumentType.REGEX) + { + json = new JNode(text); + lints = null; + } else { if (documentType == DocumentType.JSONL) @@ -547,7 +562,7 @@ public static (bool fatal, JNode node, bool usesSelections, DocumentType Documen } else { - // it's a selection-based document, so type of document doesn't matter + // it's a selection-based document, so for regex documents, each selection is just a string JNode, and otherwise we parse each selection as JSON json = new JObject(); JObject obj = (JObject)json; lints = new List(); @@ -559,7 +574,7 @@ public static (bool fatal, JNode node, bool usesSelections, DocumentType Documen foreach ((int start, int end) in selRanges) { string selRange = Npp.GetSlice(start, end); - JNode subJson = jsonParser.Parse(selRange); + JNode subJson = documentType == DocumentType.REGEX ? new JNode(selRange, start) : jsonParser.Parse(selRange); string key = $"{start},{end}"; obj[key] = subJson; lints.AddRange(jsonParser.lint); @@ -612,7 +627,7 @@ public static (bool fatal, JNode node, bool usesSelections, DocumentType Documen Npp.notepad.SetStatusBarSection($"JSON with fatal errors - {lintCount} errors (Alt-P-J-E to view)", StatusBarSection.DocType); } - else if (!(!info.usesSelections && documentType == DocumentType.INI)) // leave ini file status bar alone + else if (!(!info.usesSelections && documentType != DocumentType.JSON && documentType != DocumentType.JSONL)) // only touch status bar if whole doc is parsed as JSON or JSON Lines { string doctypeDescription; switch (jsonParser.state) @@ -703,7 +718,7 @@ public static bool UseComments(JsonFileInfo info) => ///
public static void PrettyPrintJson() { - (bool fatal, JNode json, bool usesSelections, DocumentType documentType) = TryParseJson(); + (bool fatal, JNode json, bool usesSelections, DocumentType documentType) = TryParseJson(DocumentType.JSON); if (fatal || json == null || !TryGetInfoForFile(activeFname, out JsonFileInfo info)) return; bool isJsonLines = info.documentType == DocumentType.JSONL; @@ -734,7 +749,7 @@ public static void PrettyPrintJson() ///
public static void CompressJson() { - (bool fatal, JNode json, bool usesSelections, DocumentType _) = TryParseJson(); + (bool fatal, JNode json, bool usesSelections, DocumentType _) = TryParseJson(DocumentType.JSON); if (fatal || json == null || !TryGetInfoForFile(activeFname, out JsonFileInfo info)) return; Func formatter; @@ -761,6 +776,7 @@ public static void CompressJson() public static Dictionary ReformatFileWithJson(JNode json, Func formatter, bool usesSelections) { var keyChanges = new Dictionary(); + JsonFileInfo info; if (usesSelections) { var obj = (JObject)json; @@ -789,7 +805,7 @@ public static void CompressJson() Npp.editor.EndUndoAction(); json = obj; } - else if (TryGetInfoForFile(activeFname, out JsonFileInfo info) && info.documentType == DocumentType.INI && json is JObject obj) + else if (TryGetInfoForFile(activeFname, out info) && info.documentType == DocumentType.INI && json is JObject obj) { string iniText; try @@ -802,15 +818,15 @@ public static void CompressJson() return keyChanges; } Npp.editor.SetText(iniText); - Npp.SetLangIni(false, false); } else { string newText = formatter(json); Npp.editor.SetText(newText); - Npp.SetLangJson(); } - AddJsonForFile(activeFname, json); + info = AddJsonForFile(activeFname, json); + if (!usesSelections) + Npp.SetLangBasedOnDocType(false, false, info.documentType); Npp.RemoveTrailingSOH(); lastEditedTime = DateTime.MaxValue; // avoid redundant parsing IsCurrentFileBig(); @@ -1092,6 +1108,8 @@ private static void RestyleEverything() FormStyle.ApplyStyle(sortForm, settings.use_npp_styling); if (grepperForm != null && !grepperForm.IsDisposed) FormStyle.ApplyStyle(grepperForm, settings.use_npp_styling); + if (regexSearchForm != null && !regexSearchForm.IsDisposed) + FormStyle.ApplyStyle(regexSearchForm, settings.use_npp_styling); string[] keys = jsonFileInfos.Keys.ToArray(); List keysToRemove = new List(); foreach (string fname in keys) @@ -1335,7 +1353,7 @@ public static void OpenJsonTree(DocumentType documentType = DocumentType.JSON) } if (!TryGetInfoForFile(activeFname, out JsonFileInfo info)) { - info = new JsonFileInfo(); + info = new JsonFileInfo(documentType:documentType); } if (info.tv != null && !info.tv.IsDisposed) { @@ -1437,27 +1455,6 @@ static void ShowAboutForm() aboutForm.Focus(); } - /// - /// parse a schema file, warning the user and returning null if parsing failed - /// - /// - /// - public static JNode ParseSchemaFile(string schema_path) - { - string schema_text = File.ReadAllText(schema_path); - JNode schema; - try - { - schema = jsonParser.Parse(schema_text); - return schema; - } - catch (Exception ex) - { - MessageBox.Show($"While trying to parse the schema at path {schema_path}, the following error occurred:\r\n{ex}", "error while trying to parse schema", MessageBoxButtons.OK, MessageBoxIcon.Error); - return null; - } - } - /// /// Prompt the user to choose a locally saved JSON schema file, /// parse the JSON schema,

@@ -1480,11 +1477,9 @@ static void ValidateJson(string schema_path = null, bool message_on_success = tr return; schema_path = openFileDialog.FileName; } - if (!schemaCache.Get(schema_path, out var validator)) - { - schemaCache.Add(schema_path); - validator = schemaCache[schema_path]; - } + JsonSchemaValidator.ValidationFunc validator; + if (!schemaCache.Get(schema_path, out validator) && !schemaCache.TryAdd(schema_path, out validator)) + return; JsonSchemaValidator.ValidationProblem? problem; try { @@ -1643,7 +1638,7 @@ static void ParseSchemasToFnamePatternsFile() schemasToFnamePatterns.children.Remove(fname); continue; } - schemaCache.Add(fname); + schemaCache.TryAdd(fname, out _); JArray patterns = (JArray)schemasToFnamePatterns[fname]; JArray regexes = new JArray(); foreach (JNode patternNode in patterns.children) @@ -1809,18 +1804,22 @@ public static void SelectAllChildren(IEnumerable positions) public static void RegexSearchToJson() { - if (regexSearchForm == null) - { - regexSearchForm = new RegexSearchForm(); - regexSearchForm.Show(); - } - else if (!regexSearchForm.Focused) + if (regexSearchForm != null && regexSearchForm.Focused) { - regexSearchForm.GrabFocus(); + Npp.editor.GrabFocus(); } else { - Npp.editor.GrabFocus(); + if (regexSearchForm == null) + { + regexSearchForm = new RegexSearchForm(); + regexSearchForm.Show(); + } + if (!regexSearchForm.Focused) + { + regexSearchForm.GrabFocus(); + } + OpenJsonTree(DocumentType.REGEX); } } #endregion // more_helper_functions @@ -1872,16 +1871,16 @@ public static void IsCurrentFileBig() ///
internal class SchemaCache { - LruCache> cache; + LruCache cache; Dictionary lastRetrieved; public SchemaCache(int capacity) { - cache = new LruCache>(capacity); + cache = new LruCache(capacity); lastRetrieved = new Dictionary(); } - public bool Get(string fname, out Func validator) + public bool Get(string fname, out JsonSchemaValidator.ValidationFunc validator) { validator = null; if (!cache.cache.ContainsKey(fname)) return false; @@ -1891,20 +1890,33 @@ public bool Get(string fname, out Func - /// read the schema file, parse it, and cache the compiled schema + /// read the schema file, parse it and compile it to a validator, cache the validator, then return true

+ /// if compiling or parsing of the schema fails, return false /// /// - public void Add(string fname) + public bool TryAdd(string fname, out JsonSchemaValidator.ValidationFunc validator) { - JNode schema = Main.ParseSchemaFile(fname); - if (schema == null) return; + string schema_text = File.ReadAllText(fname); + validator = null; + JNode schema; + try + { + schema = Main.jsonParser.Parse(schema_text); + } + catch (Exception ex) + { + MessageBox.Show($"While trying to parse the schema at path {fname}, the following error occurred:\r\n{RemesParser.PrettifyException(ex)}", "error while trying to parse schema", MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; + } + if (schema == null) + return false; if (cache.isFull) { // the cache is about to have its oldest key purged @@ -1912,11 +1924,23 @@ public void Add(string fname) string lastFnameAdded = cache.OldestKey(); lastRetrieved.Remove(lastFnameAdded); } - lastRetrieved[fname] = DateTime.Now; - cache[fname] = JsonSchemaValidator.CompileValidationFunc(schema); + try + { + validator = JsonSchemaValidator.CompileValidationFunc(schema); + lastRetrieved[fname] = DateTime.Now; + cache[fname] = validator; + return true; + } + catch (Exception ex) + { + MessageBox.Show($"While compiling schema for file \"{fname}\", got exception {RemesParser.PrettifyException(ex)}", + "error while compiling JSON schema", + MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; + } } - public Func this[string fname] + public JsonSchemaValidator.ValidationFunc this[string fname] { get { return cache[fname]; } } diff --git a/JsonToolsNppPlugin/Properties/AssemblyInfo.cs b/JsonToolsNppPlugin/Properties/AssemblyInfo.cs index f922424..249724c 100644 --- a/JsonToolsNppPlugin/Properties/AssemblyInfo.cs +++ b/JsonToolsNppPlugin/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ // Build Number // Revision // -[assembly: AssemblyVersion("5.8.0.13")] -[assembly: AssemblyFileVersion("5.8.0.13")] +[assembly: AssemblyVersion("5.8.0.14")] +[assembly: AssemblyFileVersion("5.8.0.14")] diff --git a/JsonToolsNppPlugin/Tests/Benchmarker.cs b/JsonToolsNppPlugin/Tests/Benchmarker.cs index 652f41c..5db9c85 100644 --- a/JsonToolsNppPlugin/Tests/Benchmarker.cs +++ b/JsonToolsNppPlugin/Tests/Benchmarker.cs @@ -258,7 +258,7 @@ public static bool BenchmarkRandomJsonAndSchemaValidation(int num_trials) watch.Reset(); watch.Start(); // validation will succeed because the tweets are generated from that schema - Func validator; + JsonSchemaValidator.ValidationFunc validator; try { validator = JsonSchemaValidator.CompileValidationFunc(tweetSchema); diff --git a/JsonToolsNppPlugin/Tests/JsonSchemaValidatorTests.cs b/JsonToolsNppPlugin/Tests/JsonSchemaValidatorTests.cs index c4881d6..1c636ef 100644 --- a/JsonToolsNppPlugin/Tests/JsonSchemaValidatorTests.cs +++ b/JsonToolsNppPlugin/Tests/JsonSchemaValidatorTests.cs @@ -272,6 +272,24 @@ public static bool Test() "{\"type\": \"array\", \"items\": {\"type\": \"string\", \"enum\": [\"one\", \"two\"]}}", false // test string enum with non-string values ), + new SchemaValidatesJson( + "[\"one\", 1.0, \"two\", 2]", + "{\"type\": \"array\", \"items\": {\"enum\": [\"one\", \"two\", 1.0, 2]}}", + true // test enum with mixed types + ), + new SchemaValidatesJson( + "[\"one\", 1.0, \"two\", 2]", + "{\"type\": \"object\", \"items\": {\"enum\": [\"one\", \"three\", 1.0, 2]}}", + false // test enum with mixed types, where a value doesn't match + ), + new SchemaValidatesJson( + "[\"one\", 1, \"two\", 2]", + "{\"type\": \"object\", \"items\": {\"enum\": [\"one\", \"two\", 1.0, 2]}}", + false // test enum with mixed types, where a value is equal but has the wrong type + ), + /** + * minItems and maxItems keywords + **/ new SchemaValidatesJson( "[1, 2, 3]", "{\"type\": \"array\", \"items\": {\"type\": \"integer\"}, \"minItems\": 1}", @@ -302,6 +320,9 @@ public static bool Test() "{\"type\": \"array\", \"items\": {\"type\": \"integer\"}, \"maxItems\": 4}", true // test maxItems with exactly valid length ), + /** + * misc tests + **/ new SchemaValidatesJson( "[1, 2, [3, 4.0], \"5\"]", "{\"type\": \"array\", \"items\": {\"anyOf\": [{\"type\": [\"string\", \"integer\"]}, {\"type\": \"array\", \"items\": {\"type\": \"number\"}}]}}", diff --git a/JsonToolsNppPlugin/Tests/RemesPathTests.cs b/JsonToolsNppPlugin/Tests/RemesPathTests.cs index 33d9933..eb6dc56 100644 --- a/JsonToolsNppPlugin/Tests/RemesPathTests.cs +++ b/JsonToolsNppPlugin/Tests/RemesPathTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -480,6 +481,13 @@ public static bool Test() new Query_DesiredResult("s_fa(`a&b&c\\nfoo&#b&ar#&##`, csv_regex(@.foo[0][2] + 1, `&`, `\\n`, `#`))", "[[\"a\", \"b\", \"c\"], [\"foo\", \"#b&ar#\", \"##\"]]"), // this is different from calling s_csv(3, `&`, `\n`, `#`) on the same input, because s_fa doesn't do the postprocessing of strings with quote characters new Query_DesiredResult("str(csv_regex(5, , , `'`))", JNode.StrToString(JNode.StrToString(ArgFunction.CsvRowRegex(5, quote:'\''), true), true)), + new Query_DesiredResult("to_csv(@.foo)", "\"0,1,2\\r\\n3.0,4.0,5.0\\r\\n6.0,7.0,8.0\\r\\n\""), + new Query_DesiredResult("to_csv(j`[{\"a\\\\tb\": 1, \"c\": null}, {\"a\\\\tb\": -7.5, \"c\": \"bar\\\\n'baz'\"}]`, `\\t`, `\\r`, `'`)", + "\"'a\\tb'\\tc\\r1\\tnull\\r-7.5\\t'bar\\n''baz'''\\r\""), + new Query_DesiredResult("to_csv(@.foo[:2], `.`, `\\n`, `#`)", "\"0.1.2\\n#3.0#.#4.0#.#5.0#\\n\""), + new Query_DesiredResult("to_csv(@.foo[1], `.`)", "\"\\\"3.0\\\"\\r\\n\\\"4.0\\\"\\r\\n\\\"5.0\\\"\""), + new Query_DesiredResult("to_csv(@.foo[:][0],,`\\n`)", "\"0\\n3.0\\n6.0\""), + new Query_DesiredResult("to_csv(@.foo[:]{a: @[1]})", "\"a\\r\\n1\\r\\n4.0\\r\\n7.0\""), // ===================== s_csv CSV parser ======================== // 3-column 14 rows, ',' delimiter, CRLF newline, '"' quote character, newline before EOF new Query_DesiredResult("s_csv(`nums,names,cities\\r\\n" + @@ -808,6 +816,53 @@ public static bool Test() $"but instead got {result.ToString()}."); } } + /** + * Test s_csv and s_fa caching (only need to use s_csv, because s_fa uses the same caching system) + **/ + string bigCsvChunk = "nums,names,cities,date,zone,subzone,contaminated\r\nnan,Bluds,BUS,,1,a,TRUE\r\n0.5,dfsd,FUDG,12/13/2020 0:00,2,c,TRUE\r\n0.5,guzo,FUDG,12/13/2020 0:00,2,d,FALSE\r\n0.5,blah,FUDG,12/13/2020 0:00,2,e,FALSE\r\n1.2,qere,GOLAR,,3,f,TRUE\r\n1.2,kijg,GOLAR,,3,h,TRUE\r\n3.4,flodt,\"q,tun\",,4,q,FALSE\r\n4.6,Kjond,YUNOB,10/17/2014 0:00,5,w,TRUE\r\n4.6,Honiu,YUNOB,10/17/2014 0:00,5,z,FALSE\r\n7,Unyir,MOKJI,5/11/2017 0:00,6,i,TRUE\r\n"; + int bigCsvChunksNeededToUseCache = ArgFunction.MIN_DOC_SIZE_CACHE_REGEX_SEARCH / bigCsvChunk.Length + 1; + JNode bigCsvNode = new JNode(ArgFunction.StrMulHelper(bigCsvChunk, bigCsvChunksNeededToUseCache)); + bool hasShownBigCsvChunk = false; + Func CheckCacheTests = (string query, JNode correct) => + { + JNode queryResult = remesparser.Search(query, bigCsvNode); + if (!queryResult.TryEquals(correct, out _)) + { + tests_failed++; + if (!hasShownBigCsvChunk) + { + hasShownBigCsvChunk = true; + Npp.AddLine($"s_csv and s_fa cache tests use bigCsv = ({bigCsvChunksNeededToUseCache} consecutive instances of {JNode.StrToString(bigCsvChunk, true)}) as input"); + } + Npp.AddLine($"Expected remesparser.Search({query}, bigCsv) to return {correct.ToString()}, but instead got {queryResult.ToString()}."); + } + return true; + }; + try + { + // test s_csv caching on input (not compile-time constant) + ii += 5; + string zutenQuery = "var zuten = s_csv(@, 7,,,,,0,4)[:12][is_num(@[0])]; zuten[:][2]"; + string correctZutenQueryStr = "[\"FUDG\",\"FUDG\",\"FUDG\",\"GOLAR\",\"GOLAR\",\"q,tun\",\"YUNOB\",\"YUNOB\",\"MOKJI\"]"; + JNode correctZutenQueryResult = jsonParser.Parse(correctZutenQueryStr); + // use non-mutating query to populate cache + CheckCacheTests(zutenQuery, correctZutenQueryResult); + string correctZutenMutationResultStr = "[\"SCHWEIN\",\"SCHWEIN\",\"SCHWEIN\",\"BLEBEN\",\"BLEBEN\",\"BLEBEN\",\"BLEBEN\",\"BLEBEN\",\"BLEBEN\"]"; + JNode correctZutenMutationResult = jsonParser.Parse(correctZutenMutationResultStr); + string firstZutenMutationQuery = "var blah = s_csv(@, 7,null,,,,0,4)[:12][is_num(@[0])]; blah[:][2] = ifelse(s_len(@)>4, BLEBEN, SCHWEIN); blah[:][2]"; + CheckCacheTests(firstZutenMutationQuery, correctZutenMutationResult); + // if the cached value can be mutated, executing s_csv twice on the same input with the same args will return different things the second time + string secondZutenMutationQuery = "var dude = s_csv(@, 7,`,`,,,,0,4)[:12][is_num(@[0])]; dude[:][2] = ifelse(s_len(@)>4, BLEBEN, SCHWEIN); dude[:][2]"; + CheckCacheTests(secondZutenMutationQuery, correctZutenMutationResult); + // use the same mutation query again to test how s_csv caching interacts with RemesParser's own LRU cache (which kicks in now) + CheckCacheTests(secondZutenMutationQuery, correctZutenMutationResult); + // one last execution of original query to check that cache wasn't mutated + CheckCacheTests(zutenQuery, correctZutenQueryResult); + } + catch (Exception ex) + { + Npp.AddLine($"While testing caching for s_csv and s_fa, got exception {ex}"); + } Npp.AddLine($"Failed {tests_failed} tests."); Npp.AddLine($"Passed {ii - tests_failed} tests."); return tests_failed > 0; diff --git a/JsonToolsNppPlugin/Tests/UserInterfaceTests.cs b/JsonToolsNppPlugin/Tests/UserInterfaceTests.cs index eebff55..5983ba0 100644 --- a/JsonToolsNppPlugin/Tests/UserInterfaceTests.cs +++ b/JsonToolsNppPlugin/Tests/UserInterfaceTests.cs @@ -22,6 +22,8 @@ public class UserInterfaceTester private static RemesParser remesParser = new RemesParser(); + private static JsonParser jsonParser = new JsonParser(); + /// /// Run a command (see below switch statement for options) to manipulate the test file. /// Returns true if one of the "compare" commands (compare_text, compare_selections, compare_treeview) fails the test @@ -39,7 +41,8 @@ public static bool ExecuteFileManipulation(string command, List messages { case "file_open": int fileIdx = (int)args[0]; - string filename = OpenUITestFile(fileIdx); + string ext = (args.Length > 1 && args[1] is string extension) ? extension : "json"; + string filename = OpenUITestFile(fileIdx, ext); messages.Add($"Opened file {filename}"); break; case "overwrite": @@ -265,6 +268,17 @@ public static bool ExecuteFileManipulation(string command, List messages else messages.Add($"Passed test of path to position {position}"); break; + case "regex_search": + if (Main.regexSearchForm == null) + Main.RegexSearchToJson(); + string regexSearchFormSettingsAsJObjectStr = (string)args[0]; + JNode regexSearchFormSettings = jsonParser.Parse(regexSearchFormSettingsAsJObjectStr); + if (Main.regexSearchForm.SetFieldsFromJson(regexSearchFormSettings, false, out string regexSettingsErrorMsg)) + messages.Add($"Opened regex search form with settings {regexSearchFormSettingsAsJObjectStr}"); + else + messages.Add(regexSettingsErrorMsg); + Main.regexSearchForm.SearchButton.PerformClick(); + break; default: throw new ArgumentException($"Unrecognized command {command}"); } @@ -589,6 +603,99 @@ public static bool ExecuteFileManipulation(string command, List messages "end for" }), ("compare_text", new object[]{"[\r\n \"0.25 is champion!0.75 is champion!\",\r\n \"0.75 is champion!0.25 is champion!\",\r\n 0.5\r\n]blah[\r\n 0,\r\n \"1.25 is champion!1.25 is champion!\"\r\n][\r\n \"boo is champion!c\",\r\n \"aa\",\r\n \"cboo is champion!\"\r\n]"}), + // TEST REGEX SEARCH FORM (REGEX MODE) + ("file_open", new object[]{2, "txt"}), + ("overwrite", new object[]{"\u200a\u202f\n foo: 1\nBA\u042f: \u00a0-3\nbaz: +85"}), + ("regex_search", new object[]{ + "{\"csv\":false,\"regex\":\"^\\\\x20*([a-zЯ]+):\\\\s+(INT)\",\"ignoreCase\":true,\"fullMatch\":false,\"numCols\":[1]}" + }), + ("treenode_click", new object[]{new string[] {"0 : [2]"} }), + ("compare_selections", new object[]{new string[] {"7,7"} }), + ("treenode_click", new object[]{new string[] {"1 : [2]", "1 : -3"} }), + ("compare_selections", new object[]{new string[] {"23,23"} }), + ("select_treenode_json", new object[]{}), + ("compare_selections", new object[]{new string[] {"23,25"} }), + ("treenode_click", new object[]{new string[] {"2 : [2]", "1 : 85"} }), + ("compare_selections", new object[]{new string[] {"31,31"} }), + ("regex_search", new object[]{ + "{\"csv\":false,\"regex\":\"^\\\\x20*([a-zЯ]+):\\\\s+(NUMBER)\",\"ignoreCase\":false,\"fullMatch\":true}" + }), + ("treenode_click", new object[]{new string[] {"1 : [3]", "0 : \"baz: +85\""} }), + ("treenode_click", new object[]{new string[] {"0 : [3]", "1 : \"foo\""} }), + ("treenode_click", new object[]{new string[] {"0 : [3]", "2 : \"1\""} }), + ("tree_query", new object[]{ + "@ = s_sub(@,\r\n" + + " g`(?i)^\\x20*([a-zЯ]+):\\s+(INT)`,\r\n" + + " ifelse(float(@[2]) < 0,\r\n" + + " str(loop()) + @[0],\r\n" + + " str(loop()) + `. ` + @[1] + ` (` + str(int(@[2])) + `)`\r\n" + + " )\r\n" + + ")"}), + ("compare_text", new object[]{"\u200a\u202f\n1. foo (1)\n2BA\u042f: \u00a0-3\n3. baz (85)"}), + // TEST REGEX SEARCH FORM (CSV MODE) + ("overwrite", new object[]{ + "ab\tc\t😀\tf\r\n" + + "'fo\nö'\t7\tbar\t-8.5\r\n" + + "baz\t19\t''\t-4e3\r\n" + + "zorq\t\tkywq\t.75" + }), + ("regex_search", new object[] + { + "{\"csv\":true,\"delim\":\"\\\\t\",\"quote\":\"'\",\"newline\":\"\\r\\n\",\"header\":\"d\",\"nColumns\":4,\"numCols\":[-1,1]}" + }), + ("treenode_click", new object[]{new string[]{"0 : {4}", "ab : \"fo\\nö\"" } }), + ("treenode_click", new object[]{new string[]{"1 : {4}", "c : 19"} }), + ("treenode_click", new object[]{new string[]{"1 : {4}", "😀 : \"\"" } }), + ("treenode_click", new object[]{new string[]{"2 : {4}", "f : 0.75"} }), + ("compare_selections", new object[]{new string[] {"60,60"} }), + ("regex_search", new object[] + { + "{\"csv\": true, \"delim\": \"\\\\t\", \"quote\": \"'\", \"newline\": \"\\r\\n\", \"header\": 1, \"nColumns\": 4, \"numCols\": [1]}" + }), + ("treenode_click", new object[]{new string[] {"0 : [4]", "3 : \"f\""} }), + ("treenode_click", new object[]{new string[] {"1 : [4]", "1 : 7"} }), + ("treenode_click", new object[]{new string[] {"2 : [4]", "3 : \"-4e3\""} }), + ("treenode_click", new object[]{new string[] {"3 : [4]", "0 : \"zorq\""} }), + // TEST SELECT ALL CHILDREN IN CSV FILE + ("tree_query", new object[]{"s_csv(@, 4, `\\t`, `\\r\\n`, `'`, h)[:][0]"}), + ("treenode_click", new object[]{new string[]{ } }), // click root + ("select_treenode_json_children", new object[]{}), + ("compare_selections", new object[]{new string[] {"0,2", "13,20", "33,36", "49,53"} }), + ("treenode_click", new object[]{new string[]{ "1 : \"fo\\nö\"" } }), + ("select_treenode_json", new object[]{}), + ("compare_selections", new object[]{new string[] {"13,20"} }), + // MUTATE CSV FILE WITH QUERY + ("tree_query", new object[]{ + "var c = s_csv(@, 4, `\\t`, `\\r\\n`, `'`, h, 1);\r\n" + + "c[:][1][is_num(@)] = @/4;\r\n" + + "@ = to_csv(c,`,`, `\\n`, `\"`)"}), + ("compare_text", new object[]{"ab,c,😀,f\n\"fo\nö\",1.75,bar,-8.5\nbaz,4.75,,-4e3\nzorq,,kywq,.75\n"}), + // TEST REGEX SEARCH FORM (SINGLE-CAPTURE-GROUP CSV AND REGEX MODES) + ("overwrite", new object[]{" 草1\nェ2\n◐3"}), + ("regex_search", new object[]{ + "{\"csv\":false,\"regex\":\"^\\\\S\\\\d\\\\r?$\",\"ignoreCase\":true,\"fullMatch\":false}" + }), + ("treenode_click", new object[]{new string[] { "1 : \"◐3\"" } }), + ("compare_selections", new object[]{new string[] {"11,11"} }), + ("select_treenode_json", new object[]{}), + ("compare_selections", new object[]{new string[] {"11,15"} }), + ("regex_search", new object[]{ + "{\"csv\":false,\"regex\":\"^\\\\S(\\\\d)\\\\r?$\",\"ignoreCase\":true,\"fullMatch\":false}" // this time capture just the number after the nonspace + }), + ("treenode_click", new object[]{new string[] { "1 : \"3\"" } }), + ("compare_selections", new object[]{new string[] {"14,14"} }), + ("regex_search", new object[] + { + "{\"csv\": true, \"delim\": \",\", \"quote\": \"'\", \"newline\": \"\\n\", \"header\": 0, \"nColumns\": 1}" + }), + ("treenode_click", new object[]{new string[] { "0 : \"ェ2\"" } }), + ("compare_selections", new object[]{new string[] {"6,6"} }), + ("regex_search", new object[] + { + "{\"csv\": true, \"delim\": \",\", \"quote\": \"'\", \"newline\": 1, \"header\": 2, \"nColumns\": 1}" + }), + ("treenode_click", new object[]{new string[] { "0 : {1}", " 草1 : \"ェ2\"" } }), + ("compare_selections", new object[]{new string[] {"6,6"} }), }; public static bool Test() @@ -597,7 +704,7 @@ public static bool Test() int failures = 0; string previouslyOpenFname = Npp.notepad.GetCurrentFilePath(); filenamesUsed = new List(); - string UITestFileName = OpenUITestFile(0); + string UITestFileName = OpenUITestFile(0, "json"); PrettyPrintStyle previousPrettyPrintStyle = Main.settings.pretty_print_style; bool previousSortKeys = Main.settings.sort_keys; @@ -632,7 +739,7 @@ public static bool Test() try { string oneArray2000xStr = (string)remesParser.Search("@ * 2000", new JNode("[1]\r\n")).value; - JArray oneArray2000x = (JArray)new JsonParser().ParseJsonLines(oneArray2000xStr); + JArray oneArray2000x = (JArray)jsonParser.ParseJsonLines(oneArray2000xStr); string[] oneArray2000xSelections = ((JArray)remesParser.Search("(range(2000) * 5)[:]{@, @ + 3}{s_join(`,`, str(@))}[0]", new JNode())).children .Select(x => (string)x.value) .ToArray(); @@ -698,13 +805,13 @@ public static bool Test() return failures > 0; } - private static string UITestFilename(int ii) { return $"UI test {ii}.json"; } + private static string UITestFilename(int ii, string extension) { return $"UI test {ii}.{extension}"; } - private static string OpenUITestFile(int fileIdx) + private static string OpenUITestFile(int fileIdx, string extension) { while (fileIdx >= filenamesUsed.Count) { - string newFilename = UITestFilename(lowestFilenameNumberNotUsed); + string newFilename = UITestFilename(lowestFilenameNumberNotUsed, extension); if (Npp.notepad.GetOpenFileNames().Contains(newFilename)) { lowestFilenameNumberNotUsed++; diff --git a/docs/README.md b/docs/README.md index d547529..bf4377e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -101,7 +101,7 @@ Error reporting can be customized with the `logger_level` setting, which has 5 l * Python-style '#' comments * Python constants `None`, *`nan`, and `inf` (starting in [5.4.0](/CHANGELOG.md#540---2023-07-04))*. * missing commas between array members - * missing ']' or '}' at the ends of arrays and objects (supported for a long time, but *JsonTools got much better at this beginning in [v6.0](/CHANGELOG.md#600---unreleased-2023-mm-dd)*) + * missing ']' or '}' at the ends of arrays and objects (supported for a long time, but *JsonTools got much better at this beginning in [v6.0](/CHANGELOG.md#600---unreleased-2023-mm-dd), allowing proper handling of e.g. `[{"a": 1, "b": "a", {"a": 2, "b": "b"}]`*) * a bunch of other common syntax errors 6. __FATAL__: These errors always cause *immediate failure* of parsing. Examples include: * unquoted string literals other than `true`, `false`, `null`, `NaN`, `Infinity`, `None`, `True`, `False`, `nan`, `inf` and `undefined`. @@ -541,9 +541,25 @@ Finally, let's sort the whole document from largest to smallest by a query on ea Of course, there's also the default sort, which can only compare numbers to numbers and strings to strings. Any mixing of types with the default sort results in failure. +## Regex search form ## + +*Added in [v6.0](/CHANGELOG.md#600---unreleased-2023-mm-dd)* + +The regex search form (`Alt-P-J-X` using accelerator keys) makes the treeview usable for any document! + +Opening up a document in regex mode allows __querying and mutating the raw text of a document with [RemesPath](/docs/RemesPath.md).__ Clicking the `Search` button on the regex search form creates a RemesPath query in the treeview for the current document using the [`s_csv` or `s_fa` functions](/docs/RemesPath.md#vectorized-functions). See the documentation for those functions for more information on the allowed regular expression syntax, but *remember that the regular expression syntax used here is not the same as Notepad++'s find-replace form.* + +![](/docs/regex%20search%20form%20regex%20example.PNG) + +You can view CSV files (any delimiter, quote character, and newline are allowed) with the treeview, providing that they comply with [RFC 4180](https://www.ietf.org/rfc/rfc4180.txt). + +![Regex search form viewing a CSV file](/docs/regex%20search%20form%20csv%20example.PNG) + +If you want to edit your document using RemesPath, the [`s_sub` function](/docs/RemesPath.md#vectorized-functions) may prove useful for regex-replacement, and the [`to_csv` function](/docs/RemesPath.md#non-vectorized-functions) may be useful for CSV editing. + ## JSON Lines documents ## -*Added in version v3.2.0* +*Added in version [v3.2.0](/CHANGELOG.md#320---2022-09-19)* [JSON Lines](https://jsonlines.org/) documents can contain multiple valid JSON documents, provided that each is on its own line and there is exactly one line per document (with an optional empty line after the last). @@ -703,6 +719,7 @@ This tool can only validate the following keywords: * type * [anyOf](https://json-schema.org/draft/2020-12/json-schema-core.html#name-anyof) * [enum](https://json-schema.org/draft/2020-12/json-schema-validation.html#name-enum) + * beginning in [v6.0](/CHANGELOG.md#600---unreleased-2023-mm-dd), the `enum` keyword can be used with mixed-type enums, and can be used without specifying the `type` keyword. * [`definitions`, `$defs`, and `$ref`](https://json-schema.org/draft/2020-12/json-schema-core.html#name-schema-re-use-with-defs) * __Notes:__ * support added in version [4.11.2](/CHANGELOG.md#4112---2023-03-21) diff --git a/docs/RemesPath.md b/docs/RemesPath.md index ecaf1f5..e460175 100644 --- a/docs/RemesPath.md +++ b/docs/RemesPath.md @@ -564,6 +564,17 @@ This differs from `str` in that *this is not vectorized.* *Added in [v5.5.0](/CHANGELOG.md#550---2023-08-13).* + +--- +`to_csv(x: array, delimiter: string=",", newline: string="\r\n", quote_char: string="\"") -> string` + +*Added in [v6.0](/CHANGELOG.md#600---unreleased-2023-mm-dd)* + +Returns x formatted as a CSV (RFC 4180 rules as normal), according to the following rules: +* if x is an array of non-iterables, each child is converted to a string on a separate line +* if x is an array of arrays, each subarray is converted to a row +* if x is an array of objects, the keys of the first subobject are converted to a header row, and the values of every subobject become their own row. + --- `to_records(x: iterable, [strategy: str]) -> array[object]` @@ -762,6 +773,7 @@ __Notes:__ * Columns containing literal quote characters or the newline characters `\r` and `\n` must be wrapped in quotes. * When `s_csv` parses a file, quoted values are parsed without the enclosing quotes and with any internal doubled quote characters replaced with a single instance of the quote character. Thus the valid value (for `"` quote character)`"foo""bar"` would be parsed as the JSON string `"foo\"bar"` * You can pass in `null` for the 3rd, 4th, and 5th args. Any instance of `null` in those args will be replaced with the default value. +* To improve performance, this function and `s_fa` use a shared cache that maps (input, function argument) pairs to the return value of the function. Up to 8 return values can be cached, only documents between 100KB and (5MB if 32bit, else 10MB) use the cache, and the cache is disabled for mutating queries (to avoid mutating values in the cache). __Example:__ Suppose you have the JSON string `"nums,names,cities,date,zone,subzone,contaminated\nnan,Bluds,BUS,,1,'',TRUE\n0.5,dfsd,FUDG,12/13/2020 0:00,2,c,TRUE\n,qere,GOLAR,,3,f,\n1.2,qere,'GOL''AR',,3,h,TRUE\n'',flodt,'q,tun',,4,q,FALSE\n4.6,Kjond,,,,w,''\n4.6,'Kj\nond',YUNOB,10/17/2014 0:00,5,z,FALSE"` @@ -834,7 +846,7 @@ __Examples:__ 4. ``s_fa(`a 1.5 1\r\nb -3e4 2\r\nc -.2 6`, `^(\w+) (NUMBER) (INT)\r?$`,false, -2, 2)`` will return `[["a",1.5,1],["b",-30000.0,2],["c",-0.2,6]]`. This time the same input is parsed with numbers in the second-to-last and third columns because `-2` and `2` were passed as optional args. 5. ``s_fa(`a 1.5 1\r\nb -3e4 2\r\nc -.2 6`, `^(\w+) (?:NUMBER) (INT)\r?$`,false, 1)`` will return `[["a",1],["b",2],["c",6]]`. This time the same input is parsed with only two columns, because we used a noncapturing version of the number-matching regex. 6. 1. ``s_fa(`a1 b+2 c-0xF d+0x1a`, `[a-z](INT)`, true, 1)`` will return `[["a1",1],["b+2",2],["c-0xF",-15],["d+0x1a",26]]` because the third argument is `true` and there is one capture group, meaning that the matches will be represented as two-element subarrays, with the first element being the full text of the match, and the second element being the captured integer parsed as a number. -6. 1. ``s_fa(`a1 b+2 c-0xF d+0x1a`, `[a-z](?:INT)`, true)`` will return `["a1","b+2","c-0xF","d+0x1a"]` because the third argument is `true` but there are no capture groups, so an array of strings is returned instead of 1-element subarrays. +7. 1. ``s_fa(`a1 b+2 c-0xF d+0x1a`, `[a-z](?:INT)`, true)`` will return `["a1","b+2","c-0xF","d+0x1a"]` because the third argument is `true` but there are no capture groups, so an array of strings is returned instead of 1-element subarrays. ---- `s_find(x: string, sub: regex | string) -> array[string]` diff --git a/docs/regex search form csv example.PNG b/docs/regex search form csv example.PNG new file mode 100644 index 0000000000000000000000000000000000000000..9e725dd26396fac365648ab9dd57fd59c7d934e0 GIT binary patch literal 86648 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%0Df*E?bFZSyQo$VscD;|C%%!tX%$nxl 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 literal 0 HcmV?d00001 diff --git a/docs/regex search form regex example.PNG b/docs/regex search form regex example.PNG new file mode 100644 index 0000000000000000000000000000000000000000..91ec7fa8de2e2cc035a84e91f5ba69a55f64fdbc GIT binary patch literal 32252 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 literal 0 HcmV?d00001 diff --git a/most recent errors.txt b/most recent errors.txt index 7d076cd..e862a97 100644 --- a/most recent errors.txt +++ b/most recent errors.txt @@ -1,4 +1,4 @@ -Test results for JsonTools v5.8.0.13 on Notepad++ 8.5.8 64bit +Test results for JsonTools v5.8.0.14 on Notepad++ 8.5.8 64bit NOTE: Ctrl-F (regular expressions *on*) for "Failed [1-9]\d*" to find all failed tests Tests failed: YAML dumper ========================= @@ -114,7 +114,7 @@ Testing RemesPath parser and compiler The queried JSON in the RemesParser tests is named foo:{"foo": [[0, 1, 2], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]], "bar": {"a": false, "b": ["a`g", "bah"]}, "baz": "z", "quz": {}, "jub": [], "guzo": [[[1]], [[2], [3]]], "7": [{"foo": 2}, 1], "_": {"0": 0}} Failed 0 tests. -Passed 442 tests. +Passed 453 tests. ========================= Testing RemesPath throws errors on bad inputs ========================= @@ -153,7 +153,7 @@ Testing JsonSchema validator ========================= Failed 0 tests. -Passed 224 tests. +Passed 229 tests. ========================= Testing JSON tabularizer ========================= @@ -195,40 +195,40 @@ Testing UI tests ========================= Failed 0 tests -Passed 242 tests +Passed 295 tests ========================= Testing JsonParser performance ========================= Preview of json: [{"A": "Ky'c^g#~)0", "a": 1850111954, "b": 9318359041, "B": "Oyi:/ xxe2", "C": "sKCSa_^7Gg", "c": 7974777124, "d": 2670309238, "D": "0d_K)HmX!.", "E": ".uM*Z{0EJ_", "e": 6958410336, "f": 8050244728, "F": "1%SG_A!xB\t", "g": 3799657125, "G": "il1^k\\\nat*", "H": {"a": 6079042826, "b": 7292804611, "c" ... -To convert JSON string of size 89556 into JNode took 2.237 +/- 0.97 ms over 32 trials -Load times (ms): 1, 4, 1, 1, 2, 3, 1, 1, 2, 2, 1, 1, 4, 1, 1, 1, 2, 1, 1, 4, 1, 1, 1, 2, 1, 1, 4, 1, 1, 1, 2, 2 +To convert JSON string of size 89556 into JNode took 3.02 +/- 1.053 ms over 32 trials +Load times (ms): 2, 2, 2, 5, 5, 4, 4, 1, 2, 3, 3, 3, 4, 1, 1, 1, 3, 2, 3, 3, 1, 2, 3, 2, 3, 3, 4, 1, 1, 2, 3, 2 ========================= Performance tests for RemesPath (float arithmetic) ========================= -Compiling query "@[@[:].a * @[:].t < @[:].e]" into took 3.198 +/- 19.376 microseconds over 40 trials -To run pre-compiled query "@[@[:].a * @[:].t < @[:].e]" on JNode from JSON of size 89556 into took 0.041 +/- 0.017 ms over 40 trials -Query times (ms): 0.103, 0.045, 0.031, 0.046, 0.032, 0.032, 0.032, 0.039, 0.058, 0.032, 0.033, 0.054, 0.079, 0.105, 0.045, 0.032, 0.044, 0.031, 0.031, 0.041, 0.033, 0.043, 0.034, 0.032, 0.032, 0.037, 0.033, 0.032, 0.032, 0.031, 0.032, 0.035, 0.042, 0.032, 0.031, 0.031, 0.046, 0.036, 0.032, 0.032 +Compiling query "@[@[:].a * @[:].t < @[:].e]" into took 1.995 +/- 11.962 microseconds over 40 trials +To run pre-compiled query "@[@[:].a * @[:].t < @[:].e]" on JNode from JSON of size 89556 into took 0.053 +/- 0.185 ms over 40 trials +Query times (ms): 0.052, 0.031, 0.024, 0.022, 0.023, 0.022, 0.023, 0.026, 0.024, 0.023, 0.023, 0.022, 0.023, 0.024, 0.023, 1.208, 0.022, 0.021, 0.021, 0.022, 0.021, 0.021, 0.021, 0.021, 0.021, 0.022, 0.022, 0.023, 0.022, 0.022, 0.022, 0.023, 0.022, 0.022, 0.023, 0.022, 0.022, 0.023, 0.023, 0.022 Preview of result: [{"A": "Ky'c^g#~)0", "a": 1850111954, "b": 9318359041, "B": "Oyi:/ xxe2", "C": "sKCSa_^7Gg", "c": 7974777124, "d": 2670309238, "D": "0d_K)HmX!.", "E": ".uM*Z{0EJ_", "e": 6958410336, "f": 8050244728, "F": "1%SG_A!xB\t", "g": 3799657125, "G": "il1^k\\\nat*", "H": {"a": 6079042826, "b": 7292804611, "c" ... ========================= Performance tests for RemesPath (string operations) ========================= -Compiling query "@[@[:].z =~ `(?i)[a-z]{5}`]" into took 3.202 +/- 19.199 microseconds over 40 trials -To run pre-compiled query "@[@[:].z =~ `(?i)[a-z]{5}`]" on JNode from JSON of size 89556 into took 0.106 +/- 0.018 ms over 40 trials -Query times (ms): 0.166, 0.106, 0.099, 0.099, 0.101, 0.098, 0.098, 0.098, 0.097, 0.097, 0.098, 0.096, 0.097, 0.097, 0.098, 0.097, 0.098, 0.098, 0.098, 0.098, 0.096, 0.098, 0.096, 0.097, 0.098, 0.098, 0.098, 0.147, 0.156, 0.123, 0.134, 0.116, 0.117, 0.115, 0.117, 0.14, 0.092, 0.092, 0.091, 0.092 +Compiling query "@[@[:].z =~ `(?i)[a-z]{5}`]" into took 1.552 +/- 9.247 microseconds over 40 trials +To run pre-compiled query "@[@[:].z =~ `(?i)[a-z]{5}`]" on JNode from JSON of size 89556 into took 0.099 +/- 0.269 ms over 40 trials +Query times (ms): 0.085, 0.055, 0.053, 0.053, 0.054, 0.053, 0.053, 0.053, 0.053, 0.053, 0.053, 0.053, 0.051, 0.053, 0.052, 0.053, 0.052, 1.78, 0.062, 0.087, 0.083, 0.056, 0.053, 0.052, 0.052, 0.052, 0.053, 0.053, 0.052, 0.052, 0.052, 0.053, 0.052, 0.053, 0.052, 0.053, 0.052, 0.052, 0.053, 0.052 Preview of result: [{"A": "\n]o1VQ5t6g", "a": 4710024278, "b": 3268860721, "B": "g4Y7+ew^.v", "C": "NK nmax_notq, `when q=true, nmax = ` + str(nmax_q), `when q=false, nmax= ` + str(nmax_notq))" into took 7.565 +/- 45.306 microseconds over 40 trials +ifelse(nmax_q > nmax_notq, `when q=true, nmax = ` + str(nmax_q), `when q=false, nmax= ` + str(nmax_notq))" into took 4.095 +/- 24.308 microseconds over 40 trials To run pre-compiled query "var qmask = @[:].q; var nmax_q = max(@[qmask].n); var nmax_notq = max(@[not qmask].n); -ifelse(nmax_q > nmax_notq, `when q=true, nmax = ` + str(nmax_q), `when q=false, nmax= ` + str(nmax_notq))" on JNode from JSON of size 89556 into took 0.027 +/- 0.011 ms over 40 trials -Query times (ms): 0.09, 0.026, 0.025, 0.03, 0.036, 0.046, 0.025, 0.024, 0.024, 0.024, 0.024, 0.023, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024, 0.025, 0.024, 0.024, 0.024, 0.025, 0.024, 0.024, 0.024, 0.023, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024, 0.024 +ifelse(nmax_q > nmax_notq, `when q=true, nmax = ` + str(nmax_q), `when q=false, nmax= ` + str(nmax_notq))" on JNode from JSON of size 89556 into took 0.028 +/- 0.055 ms over 40 trials +Query times (ms): 0.052, 0.017, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.015, 0.016, 0.015, 0.016, 0.016, 0.016, 0.017, 0.015, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.017, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.368, 0.064, 0.035, 0.036, 0.026, 0.026, 0.024 Preview of result: "when q=false, nmax= 9830935647.0" ... ========================= @@ -267,11 +267,11 @@ Performance tests for RemesPath (references to compile-time constant variables) Compiling query "var X = X; var onetwo = j`[1, 2]`; -@[:]->at(@, X)->at(@, onetwo)" into took 3.867 +/- 23.432 microseconds over 40 trials +@[:]->at(@, X)->at(@, onetwo)" into took 2.35 +/- 14.035 microseconds over 40 trials To run pre-compiled query "var X = X; var onetwo = j`[1, 2]`; -@[:]->at(@, X)->at(@, onetwo)" on JNode from JSON of size 89556 into took 0.016 +/- 0.01 ms over 40 trials -Query times (ms): 0.054, 0.013, 0.014, 0.013, 0.015, 0.051, 0.031, 0.013, 0.013, 0.039, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.012, 0.014, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.023, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013, 0.013 +@[:]->at(@, X)->at(@, onetwo)" on JNode from JSON of size 89556 into took 0.013 +/- 0.003 ms over 40 trials +Query times (ms): 0.031, 0.013, 0.013, 0.012, 0.013, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.013, 0.012, 0.013, 0.012, 0.012, 0.012, 0.012, 0.012, 0.012, 0.013, 0.012, 0.013 Preview of result: [[1695727848, 0.287562638736685], [2126430375, 0.00767794129708177], [5310550656, 0.380769772645687], [2519183283, 0.153176220930558], [6610062385, 0.662996225870666], [987168256, 0.924410189999928], [6615003609, 0.917112691225947], [4465232046, 0.684311931851536], [8654414565, 0.631485392105992], [ ... ========================= @@ -280,29 +280,29 @@ Performance tests for RemesPath (references to variables that are not compile-ti Compiling query "var X = @->`X`; var onetwo = @{1, 2}; -@[:]->at(@, X)->at(@, onetwo)" into took 3.24 +/- 19.561 microseconds over 40 trials +@[:]->at(@, X)->at(@, onetwo)" into took 2.745 +/- 16.518 microseconds over 40 trials To run pre-compiled query "var X = @->`X`; var onetwo = @{1, 2}; -@[:]->at(@, X)->at(@, onetwo)" on JNode from JSON of size 89556 into took 0.028 +/- 0.045 ms over 40 trials -Query times (ms): 0.06, 0.025, 0.016, 0.017, 0.017, 0.016, 0.018, 0.019, 0.056, 0.034, 0.017, 0.017, 0.041, 0.017, 0.016, 0.016, 0.016, 0.016, 0.016, 0.017, 0.017, 0.016, 0.017, 0.016, 0.016, 0.017, 0.298, 0.023, 0.017, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.016, 0.04, 0.016, 0.016, 0.015 +@[:]->at(@, X)->at(@, onetwo)" on JNode from JSON of size 89556 into took 0.016 +/- 0.003 ms over 40 trials +Query times (ms): 0.037, 0.017, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015, 0.016, 0.016, 0.016, 0.016, 0.016, 0.015, 0.016, 0.016, 0.015 Preview of result: [[1695727848, 0.287562638736685], [2126430375, 0.00767794129708177], [5310550656, 0.380769772645687], [2519183283, 0.153176220930558], [6610062385, 0.662996225870666], [987168256, 0.924410189999928], [6615003609, 0.917112691225947], [4465232046, 0.684311931851536], [8654414565, 0.631485392105992], [ ... ========================= Performance tests for RemesPath (simple string mutations) ========================= -Compiling query "@[:].z = s_sub(@, g, B)" into took 2.285 +/- 13.918 microseconds over 40 trials -To run pre-compiled query "@[:].z = s_sub(@, g, B)" on JNode from JSON of size 89556 into took 0.014 +/- 0.004 ms over 40 trials -Query times (ms): 0.031, 0.02, 0.013, 0.023, 0.014, 0.009, 0.012, 0.013, 0.013, 0.014, 0.014, 0.027, 0.019, 0.014, 0.014, 0.014, 0.013, 0.015, 0.013, 0.011, 0.012, 0.011, 0.012, 0.011, 0.012, 0.01, 0.012, 0.01, 0.014, 0.014, 0.014, 0.016, 0.011, 0.012, 0.011, 0.011, 0.01, 0.011, 0.011, 0.011 +Compiling query "@[:].z = s_sub(@, g, B)" into took 1.168 +/- 6.939 microseconds over 40 trials +To run pre-compiled query "@[:].z = s_sub(@, g, B)" on JNode from JSON of size 89556 into took 0.015 +/- 0.008 ms over 40 trials +Query times (ms): 0.02, 0.018, 0.013, 0.009, 0.009, 0.025, 0.01, 0.01, 0.01, 0.012, 0.041, 0.015, 0.009, 0.01, 0.011, 0.013, 0.009, 0.008, 0.01, 0.037, 0.009, 0.009, 0.012, 0.011, 0.014, 0.011, 0.009, 0.01, 0.015, 0.009, 0.009, 0.011, 0.014, 0.038, 0.016, 0.012, 0.011, 0.032, 0.02, 0.024 Preview of result: [{"A": "Ky'c^g#~)0", "a": 1850111954, "b": 9318359041, "B": "Oyi:/ xxe2", "C": "sKCSa_^7Gg", "c": 7974777124, "d": 2670309238, "D": "0d_K)HmX!.", "E": ".uM*Z{0EJ_", "e": 6958410336, "f": 8050244728, "F": "1%SG_A!xB\t", "g": 3799657125, "G": "il1^k\\\nat*", "H": {"a": 6079042826, "b": 7292804611, "c" ... ========================= Performance tests for RemesPath (simple number mutations) ========================= -Compiling query "@[:].x = ifelse(@ < 0.5, @ + 3, @ - 3)" into took 2.327 +/- 14.087 microseconds over 40 trials -To run pre-compiled query "@[:].x = ifelse(@ < 0.5, @ + 3, @ - 3)" on JNode from JSON of size 89556 into took 0.051 +/- 0.018 ms over 40 trials -Query times (ms): 0.07, 0.068, 0.083, 0.069, 0.057, 0.057, 0.057, 0.053, 0.047, 0.03, 0.061, 0.106, 0.056, 0.074, 0.05, 0.071, 0.049, 0.03, 0.044, 0.045, 0.044, 0.043, 0.044, 0.044, 0.044, 0.04, 0.038, 0.054, 0.059, 0.047, 0.038, 0.086, 0.062, 0.032, 0.029, 0.03, 0.029, 0.031, 0.03, 0.029 +Compiling query "@[:].x = ifelse(@ < 0.5, @ + 3, @ - 3)" into took 2.15 +/- 12.786 microseconds over 40 trials +To run pre-compiled query "@[:].x = ifelse(@ < 0.5, @ + 3, @ - 3)" on JNode from JSON of size 89556 into took 0.041 +/- 0.012 ms over 40 trials +Query times (ms): 0.042, 0.026, 0.056, 0.051, 0.037, 0.022, 0.031, 0.029, 0.048, 0.053, 0.038, 0.038, 0.038, 0.057, 0.026, 0.022, 0.023, 0.023, 0.051, 0.041, 0.034, 0.038, 0.048, 0.057, 0.035, 0.041, 0.046, 0.041, 0.044, 0.08, 0.068, 0.043, 0.038, 0.03, 0.036, 0.05, 0.04, 0.041, 0.038, 0.037 Preview of result: [{"A": "Ky'c^g#~)0", "a": 1850111954, "b": 9318359041, "B": "Oyi:/ xxe2", "C": "sKCSa_^7Gg", "c": 7974777124, "d": 2670309238, "D": "0d_K)HmX!.", "E": ".uM*Z{0EJ_", "e": 6958410336, "f": 8050244728, "F": "1%SG_A!xB\t", "g": 3799657125, "G": "il1^k\\\nat*", "H": {"a": 6079042826, "b": 7292804611, "c" ... ========================= @@ -312,12 +312,12 @@ Performance tests for RemesPath (mutations with a for loop) Compiling query "var xhalf = @[:].x < 0.5; for lx = zip(@[:].l, xhalf); lx[0] = ifelse(lx[1], foo, bar); -end for;" into took 8.672 +/- 52.959 microseconds over 40 trials +end for;" into took 4.322 +/- 26.193 microseconds over 40 trials To run pre-compiled query "var xhalf = @[:].x < 0.5; for lx = zip(@[:].l, xhalf); lx[0] = ifelse(lx[1], foo, bar); -end for;" on JNode from JSON of size 89556 into took 0.047 +/- 0.024 ms over 40 trials -Query times (ms): 0.108, 0.085, 0.097, 0.158, 0.06, 0.045, 0.044, 0.044, 0.039, 0.041, 0.041, 0.04, 0.038, 0.044, 0.037, 0.038, 0.037, 0.036, 0.038, 0.034, 0.034, 0.034, 0.036, 0.038, 0.038, 0.038, 0.038, 0.038, 0.038, 0.052, 0.048, 0.038, 0.038, 0.061, 0.036, 0.036, 0.038, 0.036, 0.037, 0.038 +end for;" on JNode from JSON of size 89556 into took 0.079 +/- 0.157 ms over 40 trials +Query times (ms): 0.058, 0.037, 0.045, 0.036, 0.035, 0.036, 0.04, 0.034, 0.032, 0.033, 0.696, 0.097, 0.051, 0.039, 0.037, 0.043, 0.036, 0.035, 0.032, 0.039, 0.032, 0.033, 0.035, 0.044, 0.035, 0.034, 0.033, 0.816, 0.036, 0.033, 0.032, 0.038, 0.039, 0.034, 0.034, 0.044, 0.154, 0.055, 0.075, 0.035 Preview of result: [["bar", false], ["bar", false], ["foo", true], ["foo", true], ["foo", true], ["foo", true], ["foo", true], ["bar", false], ["bar", false], ["bar", false], ["foo", true], ["foo", true], ["bar", false], ["bar", false], ["foo", true], ["bar", false], ["bar", false], ["bar", false], ["foo", true], ["ba ... ========================= @@ -326,18 +326,18 @@ Testing performance of JSON compression and pretty-printing Preview of json: [{"A": "Ky'c^g#~)0", "a": 1850111954, "b": 9318359041, "B": "Oyi:/ xxe2", "C": "sKCSa_^7Gg", "c": 7974777124, "d": 2670309238, "D": "0d_K)HmX!.", "E": ".uM*Z{0EJ_", "e": 6958410336, "f": 8050244728, "F": "1%SG_A!xB\t", "g": 3799657125, "G": "il1^k\\\nat*", "H": {"a": 6079042826, "b": 7292804611, "c" ... -To compress JNode from JSON string of 89556 took 4.02 +/- 0.578 ms over 64 trials (minimal whitespace, sort_keys=TRUE) -To compress JNode from JSON string of 89556 took 2.887 +/- 1.025 ms over 64 trials (minimal whitespace, sort_keys=FALSE) -To Google-style pretty-print JNode from JSON string of 89556 took 4.764 +/- 1.211 ms over 64 trials (sort_keys=true, indent=4) -To Whitesmith-style pretty-print JNode from JSON string of 89556 took 4.575 +/- 0.951 ms over 64 trials (sort_keys=true, indent=4) -To PPrint-style pretty-print JNode from JSON string of 89556 took 8.026 +/- 1.91 ms over 64 trials (sort_keys=true, indent=4) +To compress JNode from JSON string of 89556 took 4.036 +/- 0.76 ms over 64 trials (minimal whitespace, sort_keys=TRUE) +To compress JNode from JSON string of 89556 took 2.483 +/- 2.128 ms over 64 trials (minimal whitespace, sort_keys=FALSE) +To Google-style pretty-print JNode from JSON string of 89556 took 4.475 +/- 0.762 ms over 64 trials (sort_keys=true, indent=4) +To Whitesmith-style pretty-print JNode from JSON string of 89556 took 4.221 +/- 0.397 ms over 64 trials (sort_keys=true, indent=4) +To PPrint-style pretty-print JNode from JSON string of 89556 took 6.551 +/- 1.174 ms over 64 trials (sort_keys=true, indent=4) ========================= Testing performance of JsonSchemaValidator and random JSON creation ========================= -To create a random set of tweet JSON of size 165748 (15 tweets) based on the matching schema took 8.813 +/- 5.2 ms over 64 trials -To compile the tweet schema to a validation function took 0.343 +/- 0.551 ms over 64 trials -To validate tweet JSON of size 165748 (15 tweets) based on the compiled schema took 1.336 +/- 0.508 ms over 64 trials +To create a random set of tweet JSON of size 144006 (15 tweets) based on the matching schema took 7.789 +/- 4.306 ms over 64 trials +To compile the tweet schema to a validation function took 0.246 +/- 0.287 ms over 64 trials +To validate tweet JSON of size 144006 (15 tweets) based on the compiled schema took 0.92 +/- 0.23 ms over 64 trials ========================= Testing JSON grepper's API request tool =========================