From 65a45e46494b3d6a8f1aa652e1698c575f910090 Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Sun, 18 Oct 2026 22:26:29 -0400 Subject: [PATCH] KBFuncs: no wrapped strpiece positions, valid JSON arrays Brings this repo's KBFuncs.nlp copies in step with the canonical visualText/spec/KBFuncs.nlp fix (VisualText/visualtext-files, same branch name). The functions were replaced by one script, and only where a copy held the known old text, so every copy got the same code: - StripEndDigits: a one-letter name only came back right because position -1 wraps since engine 4.2.15, and a name with no trailing digits lost its last letter ("item" -> "ite"). It now counts positions up from 0. - SeparateConCount (where present): only the trailing digits count ("a1b2" is "a1b" + "2", not "a1" + "12"); no position arithmetic that can go negative. - JsonKBRecurse (where present): an array followed by a plain sibling is now valid JSON, adjacent arrays with different names stay separate, and the stray hier.txt debug file is gone. Shapes that were valid JSON are written byte for byte as before; matches the jsonwrite builtin fix in VisualText/nlp-engine#796. Each analyzer was run on its inputs before and after with engine 4.2.36: output unchanged. Co-Authored-By: Claude Opus 5.5 --- date-time/spec/KBFuncs.nlp | 114 +++++++++++++++++++++--------------- formatting/spec/KBFuncs.nlp | 114 +++++++++++++++++++++--------------- nlp/spec/KBFuncs.nlp | 114 +++++++++++++++++++++--------------- 3 files changed, 201 insertions(+), 141 deletions(-) diff --git a/date-time/spec/KBFuncs.nlp b/date-time/spec/KBFuncs.nlp index d6bb5cb..58da36f 100644 --- a/date-time/spec/KBFuncs.nlp +++ b/date-time/spec/KBFuncs.nlp @@ -184,13 +184,14 @@ CountName(L("con"),L("root"),L("pad")) { } StripEndDigits(L("name")) { + # "item12" -> "item". A name with no trailing digits comes back whole, + # an all-digit name gives 0. Positions only count up from 0, so a short + # name never reaches a negative strpiece position (which counts from the + # end since engine 4.2.15). if (strisdigit(L("name"))) return 0; - L("len") = strlength(L("name")) - 1; - L("i") = L("len") - 1; - L("str") = strpiece(L("name"),L("i"),L("len")); - while (strisdigit(L("str")) && L("i")) { + L("i") = strlength(L("name")) - 1; + while (L("i") > 0 && strisdigit(strpiece(L("name"),L("i"),L("i")))) { L("i")--; - L("str") = strpiece(L("name"),L("i"),L("len")); } return strpiece(L("name"),0,L("i")); } @@ -403,65 +404,89 @@ JsonKB(L("file"),L("con")) { } JsonKBRecurse(L("file"),L("parent"),L("level")) { + # The separator is written before each child, once it is known what the + # child is. Writing it after put the comma inside an array that a plain + # sibling then closed, which was not JSON. A counted child with a new + # base name (item2 then thing1) starts its own array. L("con") = down(L("parent")); L("first") = 1; - L("has concount") = 0; + L("in array") = 0; + L("array base") = 0; while (L("con")) { L("name") = conceptname(L("con")); L("concount") = SeparateConCount(L("name")); L("connum") = G("connum"); + L("continues") = 0; + if (L("concount") && L("in array")) { + if (L("concount") == L("array base")) + L("continues") = 1; + } + + if (!L("first")) { + if (L("continues")) { + L("file") << ",\n"; + } else if (L("in array")) { + L("file") << "\n" << SpacesStr(L("level")+1) << "],\n"; + L("in array") = 0; + } else if (L("concount")) { + L("file") << ","; + } else { + L("file") << ",\n"; + } + } if (L("concount")) { - "hier.txt" << SpacesStr(L("level")+1) << " ====> " << L("connum") << "\n"; - if (!L("has concount")) { + if (!L("continues")) { L("file") << SpacesStr(L("level")+1) << JsonStr(L("concount")) << ": [\n"; - } else { - L("file") << "\n"; + L("in array") = 1; + L("array base") = L("concount"); } L("file") << SpacesStr(L("level")+2) << "{\n"; - L("file") << SpacesStr(L("level")+3) << "\"id\": \"" << L("connum") << "\",\n"; - L("first") = !JsonAttributes(L("file"),L("con"),L("level")+2); - L("has concount") = 1; - + L("file") << SpacesStr(L("level")+3) << "\"id\": \"" << L("connum") << "\""; + L("has attrs") = JsonHasAttributes(L("con")); + if (L("has attrs")) { + L("file") << ",\n"; + JsonAttributes(L("file"),L("con"),L("level")+2); + } if (down(L("con"))) { - if (!L("first")) L("file") << ",\n"; + L("file") << ",\n"; JsonKBRecurse(L("file"),L("con"),L("level")+2); } else { L("file") << "\n" << SpacesStr(L("level")+2) << "}"; } } else { - if (L("has concount")) { - L("file") << "\n" << SpacesStr(L("level")+2) << "}"; - L("file") << "\n" << SpacesStr(L("level")+1) << "]"; - L("has concount") = 0; - } - if (!L("first")) - L("file") << "\n"; L("file") << SpacesStr(L("level")+1) << JsonStr(L("name")) << ": {\n"; - L("first") = !JsonAttributes(L("file"),L("con"),L("level")+1); - + L("has attrs") = JsonAttributes(L("file"),L("con"),L("level")+1); if (down(L("con"))) { - if (!L("first")) L("file") << ",\n"; + if (L("has attrs")) L("file") << ",\n"; JsonKBRecurse(L("file"),L("con"),L("level")+1); - } - else if (!L("has concount")) { + } else { L("file") << "\n" << SpacesStr(L("level")+1) << "}"; } } L("con") = next(L("con")); - if (L("con")) { - L("file") << ","; - } L("first") = 0; } - if (L("has concount")) { + if (L("in array")) { L("file") << "\n" << SpacesStr(L("level")+1) << "]"; } L("file") << "\n" << SpacesStr(L("level")) << "}"; } +JsonHasAttributes(L("con")) { + # Would JsonAttributes write anything? An array element writes its "id" + # first and needs to know whether a comma follows it. + L("attrs") = findattrs(L("con")); + while (L("attrs")) { + L("count con") = IsCountCon(attrname(L("attrs")),L("con")); + if (!L("count con")) return 1; + L("attrs") = nextattr(L("attrs")); + } + return 0; +} + JsonAttributes(L("file"),L("con"),L("level")) { L("attrs") = findattrs(L("con")); if (!L("attrs")) { @@ -527,26 +552,21 @@ JsonStr(L("str")) { } SeparateConCount(L("conname")) { + # "item12" -> "item", with G("connum") = "12". Only the digits at the + # end count: "a1b2" is "a1b" and "2". Returns 0 when the name does not + # end in digits or is all digits. + G("connum") = 0; if (strisdigit(L("conname"))) return 0; - L("i") = strlength(L("conname")) - 1; - G("connum") = 0; - L("stop") = 0; - while (!L("stop") && L("i") > 0) { - L("c") = strpiece(L("conname"),L("i"),L("i")); - if (strisdigit(L("c"))) { - L("num") = L("c") + L("num"); - } else { - L("stop"); - } + L("last") = strlength(L("conname")) - 1; + L("i") = L("last"); + while (L("i") > 0 && strisdigit(strpiece(L("conname"),L("i"),L("i")))) { L("i")--; } - if (L("num")) { - G("connum") = L("num"); - L("base") = strpiece(L("conname"),0,strlength(L("conname"))-strlength(L("num"))-1); - return L("base"); - } - return 0; + if (L("i") == L("last")) return 0; + + G("connum") = strpiece(L("conname"),L("i")+1,L("last")); + return strpiece(L("conname"),0,L("i")); } IsCountCon(L("str"),L("con")) { diff --git a/formatting/spec/KBFuncs.nlp b/formatting/spec/KBFuncs.nlp index 451a5ec..eb85dfb 100644 --- a/formatting/spec/KBFuncs.nlp +++ b/formatting/spec/KBFuncs.nlp @@ -184,13 +184,14 @@ CountName(L("con"),L("root"),L("pad")) { } StripEndDigits(L("name")) { + # "item12" -> "item". A name with no trailing digits comes back whole, + # an all-digit name gives 0. Positions only count up from 0, so a short + # name never reaches a negative strpiece position (which counts from the + # end since engine 4.2.15). if (strisdigit(L("name"))) return 0; - L("len") = strlength(L("name")) - 1; - L("i") = L("len") - 1; - L("str") = strpiece(L("name"),L("i"),L("len")); - while (strisdigit(L("str")) && L("i")) { + L("i") = strlength(L("name")) - 1; + while (L("i") > 0 && strisdigit(strpiece(L("name"),L("i"),L("i")))) { L("i")--; - L("str") = strpiece(L("name"),L("i"),L("len")); } return strpiece(L("name"),0,L("i")); } @@ -403,65 +404,89 @@ JsonKB(L("file"),L("con")) { } JsonKBRecurse(L("file"),L("parent"),L("level")) { + # The separator is written before each child, once it is known what the + # child is. Writing it after put the comma inside an array that a plain + # sibling then closed, which was not JSON. A counted child with a new + # base name (item2 then thing1) starts its own array. L("con") = down(L("parent")); L("first") = 1; - L("has concount") = 0; + L("in array") = 0; + L("array base") = 0; while (L("con")) { L("name") = conceptname(L("con")); L("concount") = SeparateConCount(L("name")); L("connum") = G("connum"); + L("continues") = 0; + if (L("concount") && L("in array")) { + if (L("concount") == L("array base")) + L("continues") = 1; + } + + if (!L("first")) { + if (L("continues")) { + L("file") << ",\n"; + } else if (L("in array")) { + L("file") << "\n" << SpacesStr(L("level")+1) << "],\n"; + L("in array") = 0; + } else if (L("concount")) { + L("file") << ","; + } else { + L("file") << ",\n"; + } + } if (L("concount")) { - "hier.txt" << SpacesStr(L("level")+1) << " ====> " << L("connum") << "\n"; - if (!L("has concount")) { + if (!L("continues")) { L("file") << SpacesStr(L("level")+1) << JsonStr(L("concount")) << ": [\n"; - } else { - L("file") << "\n"; + L("in array") = 1; + L("array base") = L("concount"); } L("file") << SpacesStr(L("level")+2) << "{\n"; - L("file") << SpacesStr(L("level")+3) << "\"id\": \"" << L("connum") << "\",\n"; - L("first") = !JsonAttributes(L("file"),L("con"),L("level")+2); - L("has concount") = 1; - + L("file") << SpacesStr(L("level")+3) << "\"id\": \"" << L("connum") << "\""; + L("has attrs") = JsonHasAttributes(L("con")); + if (L("has attrs")) { + L("file") << ",\n"; + JsonAttributes(L("file"),L("con"),L("level")+2); + } if (down(L("con"))) { - if (!L("first")) L("file") << ",\n"; + L("file") << ",\n"; JsonKBRecurse(L("file"),L("con"),L("level")+2); } else { L("file") << "\n" << SpacesStr(L("level")+2) << "}"; } } else { - if (L("has concount")) { - L("file") << "\n" << SpacesStr(L("level")+2) << "}"; - L("file") << "\n" << SpacesStr(L("level")+1) << "]"; - L("has concount") = 0; - } - if (!L("first")) - L("file") << "\n"; L("file") << SpacesStr(L("level")+1) << JsonStr(L("name")) << ": {\n"; - L("first") = !JsonAttributes(L("file"),L("con"),L("level")+1); - + L("has attrs") = JsonAttributes(L("file"),L("con"),L("level")+1); if (down(L("con"))) { - if (!L("first")) L("file") << ",\n"; + if (L("has attrs")) L("file") << ",\n"; JsonKBRecurse(L("file"),L("con"),L("level")+1); - } - else if (!L("has concount")) { + } else { L("file") << "\n" << SpacesStr(L("level")+1) << "}"; } } L("con") = next(L("con")); - if (L("con")) { - L("file") << ","; - } L("first") = 0; } - if (L("has concount")) { + if (L("in array")) { L("file") << "\n" << SpacesStr(L("level")+1) << "]"; } L("file") << "\n" << SpacesStr(L("level")) << "}"; } +JsonHasAttributes(L("con")) { + # Would JsonAttributes write anything? An array element writes its "id" + # first and needs to know whether a comma follows it. + L("attrs") = findattrs(L("con")); + while (L("attrs")) { + L("count con") = IsCountCon(attrname(L("attrs")),L("con")); + if (!L("count con")) return 1; + L("attrs") = nextattr(L("attrs")); + } + return 0; +} + JsonAttributes(L("file"),L("con"),L("level")) { L("attrs") = findattrs(L("con")); if (!L("attrs")) { @@ -527,26 +552,21 @@ JsonStr(L("str")) { } SeparateConCount(L("conname")) { + # "item12" -> "item", with G("connum") = "12". Only the digits at the + # end count: "a1b2" is "a1b" and "2". Returns 0 when the name does not + # end in digits or is all digits. + G("connum") = 0; if (strisdigit(L("conname"))) return 0; - L("i") = strlength(L("conname")) - 1; - G("connum") = 0; - L("stop") = 0; - while (!L("stop") && L("i") > 0) { - L("c") = strpiece(L("conname"),L("i"),L("i")); - if (strisdigit(L("c"))) { - L("num") = L("c") + L("num"); - } else { - L("stop"); - } + L("last") = strlength(L("conname")) - 1; + L("i") = L("last"); + while (L("i") > 0 && strisdigit(strpiece(L("conname"),L("i"),L("i")))) { L("i")--; } - if (L("num")) { - G("connum") = L("num"); - L("base") = strpiece(L("conname"),0,strlength(L("conname"))-strlength(L("num"))-1); - return L("base"); - } - return 0; + if (L("i") == L("last")) return 0; + + G("connum") = strpiece(L("conname"),L("i")+1,L("last")); + return strpiece(L("conname"),0,L("i")); } IsCountCon(L("str"),L("con")) { diff --git a/nlp/spec/KBFuncs.nlp b/nlp/spec/KBFuncs.nlp index b7bb38e..8956e00 100644 --- a/nlp/spec/KBFuncs.nlp +++ b/nlp/spec/KBFuncs.nlp @@ -187,13 +187,14 @@ CountName(L("con"),L("root"),L("pad")) { } StripEndDigits(L("name")) { + # "item12" -> "item". A name with no trailing digits comes back whole, + # an all-digit name gives 0. Positions only count up from 0, so a short + # name never reaches a negative strpiece position (which counts from the + # end since engine 4.2.15). if (strisdigit(L("name"))) return 0; - L("len") = strlength(L("name")) - 1; - L("i") = L("len") - 1; - L("str") = strpiece(L("name"),L("i"),L("len")); - while (strisdigit(L("str")) && L("i")) { + L("i") = strlength(L("name")) - 1; + while (L("i") > 0 && strisdigit(strpiece(L("name"),L("i"),L("i")))) { L("i")--; - L("str") = strpiece(L("name"),L("i"),L("len")); } return strpiece(L("name"),0,L("i")); } @@ -406,65 +407,89 @@ JsonKB(L("file"),L("con")) { } JsonKBRecurse(L("file"),L("parent"),L("level")) { + # The separator is written before each child, once it is known what the + # child is. Writing it after put the comma inside an array that a plain + # sibling then closed, which was not JSON. A counted child with a new + # base name (item2 then thing1) starts its own array. L("con") = down(L("parent")); L("first") = 1; - L("has concount") = 0; + L("in array") = 0; + L("array base") = 0; while (L("con")) { L("name") = conceptname(L("con")); L("concount") = SeparateConCount(L("name")); L("connum") = G("connum"); + L("continues") = 0; + if (L("concount") && L("in array")) { + if (L("concount") == L("array base")) + L("continues") = 1; + } + + if (!L("first")) { + if (L("continues")) { + L("file") << ",\n"; + } else if (L("in array")) { + L("file") << "\n" << SpacesStr(L("level")+1) << "],\n"; + L("in array") = 0; + } else if (L("concount")) { + L("file") << ","; + } else { + L("file") << ",\n"; + } + } if (L("concount")) { - "hier.txt" << SpacesStr(L("level")+1) << " ====> " << L("connum") << "\n"; - if (!L("has concount")) { + if (!L("continues")) { L("file") << SpacesStr(L("level")+1) << JsonStr(L("concount")) << ": [\n"; - } else { - L("file") << "\n"; + L("in array") = 1; + L("array base") = L("concount"); } L("file") << SpacesStr(L("level")+2) << "{\n"; - L("file") << SpacesStr(L("level")+3) << "\"id\": \"" << L("connum") << "\",\n"; - L("first") = !JsonAttributes(L("file"),L("con"),L("level")+2); - L("has concount") = 1; - + L("file") << SpacesStr(L("level")+3) << "\"id\": \"" << L("connum") << "\""; + L("has attrs") = JsonHasAttributes(L("con")); + if (L("has attrs")) { + L("file") << ",\n"; + JsonAttributes(L("file"),L("con"),L("level")+2); + } if (down(L("con"))) { - if (!L("first")) L("file") << ",\n"; + L("file") << ",\n"; JsonKBRecurse(L("file"),L("con"),L("level")+2); } else { L("file") << "\n" << SpacesStr(L("level")+2) << "}"; } } else { - if (L("has concount")) { - L("file") << "\n" << SpacesStr(L("level")+2) << "}"; - L("file") << "\n" << SpacesStr(L("level")+1) << "]"; - L("has concount") = 0; - } - if (!L("first")) - L("file") << "\n"; L("file") << SpacesStr(L("level")+1) << JsonStr(L("name")) << ": {\n"; - L("first") = !JsonAttributes(L("file"),L("con"),L("level")+1); - + L("has attrs") = JsonAttributes(L("file"),L("con"),L("level")+1); if (down(L("con"))) { - if (!L("first")) L("file") << ",\n"; + if (L("has attrs")) L("file") << ",\n"; JsonKBRecurse(L("file"),L("con"),L("level")+1); - } - else if (!L("has concount")) { + } else { L("file") << "\n" << SpacesStr(L("level")+1) << "}"; } } L("con") = next(L("con")); - if (L("con")) { - L("file") << ","; - } L("first") = 0; } - if (L("has concount")) { + if (L("in array")) { L("file") << "\n" << SpacesStr(L("level")+1) << "]"; } L("file") << "\n" << SpacesStr(L("level")) << "}"; } +JsonHasAttributes(L("con")) { + # Would JsonAttributes write anything? An array element writes its "id" + # first and needs to know whether a comma follows it. + L("attrs") = findattrs(L("con")); + while (L("attrs")) { + L("count con") = IsCountCon(attrname(L("attrs")),L("con")); + if (!L("count con")) return 1; + L("attrs") = nextattr(L("attrs")); + } + return 0; +} + JsonAttributes(L("file"),L("con"),L("level")) { L("attrs") = findattrs(L("con")); if (!L("attrs")) { @@ -530,26 +555,21 @@ JsonStr(L("str")) { } SeparateConCount(L("conname")) { + # "item12" -> "item", with G("connum") = "12". Only the digits at the + # end count: "a1b2" is "a1b" and "2". Returns 0 when the name does not + # end in digits or is all digits. + G("connum") = 0; if (strisdigit(L("conname"))) return 0; - L("i") = strlength(L("conname")) - 1; - G("connum") = 0; - L("stop") = 0; - while (!L("stop") && L("i") > 0) { - L("c") = strpiece(L("conname"),L("i"),L("i")); - if (strisdigit(L("c"))) { - L("num") = L("c") + L("num"); - } else { - L("stop"); - } + L("last") = strlength(L("conname")) - 1; + L("i") = L("last"); + while (L("i") > 0 && strisdigit(strpiece(L("conname"),L("i"),L("i")))) { L("i")--; } - if (L("num")) { - G("connum") = L("num"); - L("base") = strpiece(L("conname"),0,strlength(L("conname"))-strlength(L("num"))-1); - return L("base"); - } - return 0; + if (L("i") == L("last")) return 0; + + G("connum") = strpiece(L("conname"),L("i")+1,L("last")); + return strpiece(L("conname"),0,L("i")); } IsCountCon(L("str"),L("con")) {