変奏現実

パソコンやMMORPGのことなどを思いつくまま・・・記載されている会社名・製品名・システム名などは、各社の商標、または登録商標です。

この画面は、簡易表示です

javascript

[javascript]spreadsheet4 special-effect (*$ … $*)

コメント(* … *)を独自に拡張し、(*$ … $*)の場合には特殊効果(Special Effect)を与えている。

意外と便利。

動作は、EBNFな文法テキストをEBNFパーサで解析する際には、

(*$ SEQUENCE SEPARATOR IS REQUIRED $*)           sequenceのパラメータは必ず[、]を使用する
(*$ SEQUENCE SEPARATOR IS NOT REQUIRED $*)       sequenceのパラメータは[  ]か[、]を使用する
(*$ DEBUG PRINT $*)  デバッグメッセージ用     EBNFテキストに差込みメッセージを表示させる

EBNFな文法テキストから作成したパーサで数式等を解析する際には、

(*$ NO SPACE SKIP $*) 字句判定前に空白をスキップしない
(*$ SPACE SKIP $*)  字句判定前に空白をスキップする
(*$ DEBUG PRINT $*)  デバッグメッセージ用 mapメソッドのsequenceパラメータ部に差込みメッセージを表示させる

とするつもりだったが、EBNFな文法テキストを読んでいる時に動作していたので、修正した。

Special-Text( ? … ? )として、他のEBNFテキストを参照する場合に ( ? jdoc ? :jdocを参照する)と云う感じにする予定。



[javascript]spreadsheet4 デバッグ

再帰しまくりなパーサ・コンピネータのデバッグは、ぐるぐると動き回るパーサがEBNFのどの辺を処理しようとしているのかはテキスト(target)と読取り位置(position)で把握できるけど、スタックコールにはsequenceやmapぐらいしか見えないから、非終端記号(EBNFの左辺の識別子(名前))レベルでパーサのネストの進行状態をイメージするのは難しい。

強固な防壁魔法の呪文を観て分析し解除するのに似ている。

平たく言えば、スタックコールのsequenceやmapの組み合わせを観て、虫を刈り取るイメージができるかどうかにかかっている。

大体、groupやoptionの配下にもchoiceやsequenceがダラダラと入り込むのでイメージするのは容易ではないから、サクサクと進む訳も無く、瞑想に頼るしかない。

そろそろ、そんな状況もかったるいので、

非終端記号はParser.map()(パラメータのsequence処理が成功したらコールバックを呼ぶパーサ)をよく使うのでmapのパラメータに非終端記号名を追加し、解析時にスタックし、mapのパース処理後に処理結果と非終端記号のネスト具合をログに書き出す様にセル手入力直後のパースにログを入れてみた。

async closeEntry(fSave = true) {
・・・
_exprParser.map.DEBUG = true;
const parseResult = _exprParser.parse(text);
_exprParser.map.DEBUG = false;
・・・
}
["expr_cell","logical_expr","add_sub_expr","mul_div_expr","number"]: x
["expr_cell","logical_expr","add_sub_expr","mul_div_expr","boolean"]: x
["expr_cell","logical_expr","add_sub_expr","mul_div_expr","function_def","symbol"]: ["B","1"]
["expr_cell","logical_expr","add_sub_expr","mul_div_expr","function_def"]: x
["expr_cell","logical_expr","add_sub_expr","mul_div_expr","a1_range","a1"]: ["B","1"]
["expr_cell","logical_expr","add_sub_expr","mul_div_expr","a1_range"]: x
["expr_cell","logical_expr","add_sub_expr","mul_div_expr","a1"]: ["B","1"]
["expr_cell","logical_expr","add_sub_expr","mul_div_expr"]: [{"type":"a1","address":[{"row":1,"col":2}]},[]]
["expr_cell","logical_expr","add_sub_expr"]: [{"type":"a1","address":[{"row":1,"col":2}]},[]]
["expr_cell","logical_expr"]: [{"type":"a1","address":[{"row":1,"col":2}]},null]
["expr_cell"]: ["=",{"type":"a1","address":[{"row":1,"col":2}]}]

順に見ていくと、

  • number、booleanと失敗し
  • function_def.symbolで B1 を判定したものの、
    • 多分、「(…)」が無いので、function_defを諦め、
  • a1_range.a1でB1を再度判定したものの、
    • 多分、「:」が無いのでa1_range諦め、
  • a1で渋々納得し、
  • パーサのネストを駆け戻り、
  • mul_div_expr、add_sub_expr、logical_expr、expr_cellを経て、
  • [“=”,{“type”:”a1″,”address”:[{“row”:1,”col”:2}]}]を得ている。

と大雑把に動きが見えるようになった。

しかし、多分、「xx」が無い、部分はまだ推測するしかないのでtokenやregexpパーサにも解析ログを付けたら、エラーチェック実装時に「「xx」が来ると予想されたxxx文字目に「yy」がありました。」とか出せそうな気がしたけど、

["expr_cell","o: '=","logical_expr","add_sub_expr","mul_div_expr","number","x: '[-]?([0-9]+)([.][0-9]*)?'"]: x
["expr_cell","o: '=","logical_expr","add_sub_expr","mul_div_expr","boolean","x: 'true'","x: 'false'"]: x
["expr_cell","o: '=","logical_expr","add_sub_expr","mul_div_expr","function_def","symbol","o: 'B1"]: ["B","1"]
["expr_cell","o: '=","logical_expr","add_sub_expr","mul_div_expr","function_def","x: '('"]: x
["expr_cell","o: '=","logical_expr","add_sub_expr","mul_div_expr","a1_range","a1","o: 'B1"]: ["B","1"]
["expr_cell","o: '=","logical_expr","add_sub_expr","mul_div_expr","a1_range","x: ':'"]: x
["expr_cell","o: '=","logical_expr","add_sub_expr","mul_div_expr","a1","o: 'B1"]: ["B","1"]
["expr_cell","o: '=","logical_expr","add_sub_expr","mul_div_expr","x: '*', '/'"]: [{"type":"a1","address":[{"row":1,"col":2}]},[]]
["expr_cell","o: '=","logical_expr","add_sub_expr","x: '+', '-', '&'"]: [{"type":"a1","address":[{"row":1,"col":2}]},[]]
["expr_cell","o: '=","logical_expr","x: '=', '>', '<', '>=', '<=', '<>'"]: [{"type":"a1","address":[{"row":1,"col":2}]},null]
["expr_cell","o: '="]: ["=",{"type":"a1","address":[{"row":1,"col":2}]}]

「x:’(’」 は良い感じだけど、非終端識別子の終わりにx:がいっぱい付いているのは・・・

あった方が良い様な、最後のxだけで良い様な、微妙な感じ。

先は長いなぁ~



[javascript]spreadsheet4

EBNFベースで数式の文法を書いたバージョンです。

やってみた感じでは・・・ただただ内部が面倒くさくなったダケでした。

なぜか、「1+2+3」が式として認識されるので、まだ大きなミスが残ってそう。

  • EBNF文法を解析するパーサ
    • セルフテスト
      • EBNF文法で書いたEBNFの文法のテキストを文法解析させJSONデータを得る。
        • 全文解析できればOK
        • 使用する正規表現自身で字句解析は無理っぽいので簡易なパターンが通ればOK
    • EBNF文法で書いた数式の文法のテキストを文法解析させJSONデータを得て、一部識別子の宣言にmapで解析結果をトークン化しやすくしておく。
    • 数式のテキストを文法解析させ数式のトークンリストを得る。
  • トークン関係
    • 数式のトークンリストはindexedDBのexprプロパティに格納する。
    • 数式のトークンリストで計算し結果をindexedDBのvalueプロパティに格納する。
  • 画面関係
    • 数式のトークンリストをテキスト化したものを画面のテーブルのセルにexpr属性として貼り付ける。
    • セル上にマウスポインタを置くと、ツールチップでセルの内容を確認できる。

特にセルフテストで正規表現をEBNF文法で、

regexp ::=  "/"  /.+/ "/" /[dgimsuvy]*/

と、こんな雰囲気で書くと間に隙間(空白)が入ってもOKになってしまうし、だからと云って正規表現でまとめると読みにくい上に、new RegExpが「オプションの指定が矛盾しています」と云ってくるので悩ましい。

今のところ、特定の識別子の定義で空白をスキップしたく無い場合は、no_space_skipを混ぜている。

const no_space_skip = function () {
    this.fSpaceSkip = false;
    return undefined;
}.bind(this);
・・・
//  wq_string   =   '"' /(?!\\\\)[^"]*/ '"' ;
const wq_string = _map(
    no_space_skip, '"', /(?!\\)[^"]*/, '"'
    , (parsed) => {
        parsed = parsed[1]
        return { _wq_string: `${parsed}` };
    });
・・・
sequence(...parsers) {
・・・
const func =
    (target, position) => {/*sequnce*/
        const bk = this.fSpaceSkip;  ☚空白読飛状態をバックアップ
・・・
        for(let i=0;i<parsers.length;i++) {
            parsers[i](target, position);  ☚この中でno_space_skipが空白読飛状態を変更
        }
・・・
        this.fSpaceSkip = bk;     ☚空白読飛状態を復元
        return 
    }
・・・
    return func;
}

no_space_skipを内包するsequenceとその配下での空白読飛しを停止できるが、

今のところsequenceパーサのパラメータにno_space_skipを直挿しするしか方法が無い。

EBNFテキストのsequence定義文のデリミタ「、」に「+」を加え、「+」ならconcat風に見えるので「空白読み飛ばし停止」的な感じにしたい。あと、先頭にも[ + ]が使えた方がいいかもしれない。

ps.2024/4/5

Ctrl+SでJSON形式でテーブルのデータをそのままダウンロード。そのファイルをテーブルにドロップするとアップロード。但し、indexedDBの検索が不調で連動計算がうまくいかない。setTimeoutで適当に間を開けてもダメなので、トランザクションがちゃんと終わるのを待たないとダメかな?と思ったら、参照セルアドレスの保存方法を修正漏れだった。setTimeoutも不要だった。

また数式の参照先が空っぽで動きを追っていくと、パーサのコールバック内でメソッド内の変数を更新しても、アロー関数作成時点のメソッドの変数の残像(ディープコピー)の方を更新しているみたいで当のメソッド内変数が更新されない。当初のパーサコンビネータ方式ではメソッドを呼び出す度にコールバックを作り直していた(のでちょっと遅い)支障なかったが、今回は式クラスのコンストラクタで1度だけ生成するようにしたせいで相違が出た。

/**
 * スプレッドシートの数式クラス
 */
class SpreadSheetExprParser {
    /**
     *  コンストラクタ
     * @param {SpreadSheetCustomElement} spreadSheet 
     */
    constructor(spreadSheet) {
        const _exprParser = this;
        _exprParser.className = _exprParser.name ?? _exprParser.constructor.name;
        _exprParser._spreadSheet = spreadSheet;

        this.parseEBNF(undefined);  // self test
        this.parse('=1+2+3');       // expression setup ☚ここで仮データで数式のパーサを初期設定。
    }
/**
 *  数式を構文解析する
 * @param {string} text   数式
 */
parse(text) {
    const ebnf = `  (* セルのデータ *)
    cell                ::= expr_cell | value_cell ;
    ・・・数式のEBNFテキスト・・・
    `;
    if (this.exptEbnfAnalize === undefined) {
        this.exptEbnfAnalize = this.parseEBNF(ebnf); ☚ ここで、数式用EBNFテキストを読ませて解析させる。
        ・・・・以下、解析結果をDBに保存できるようにトークン化するコールバック一式を作成。
     ※コンストラクタから呼び出された時にローカルなアロー関数は参照する変数をディープコピーしてるっぽい。
    }
    ・・・・
    //  数式の纏めパーサ
    const cell = this.exptEbnfAnalize.parser['cell']; ☚ ここで数式の文法のトップを調べ、
    var result = cell(target, 0); ☚ ここで数式を読ませ解析させる。この時点で新たなローカル変数が作られ、
                                      上記のローカルなアロー関数がアクセスするローカル変数とは別腹。
    if (!result.success) {
    ・・・・

仕方が無いので解析結果からA1やA1範囲のトークンを探すことにした。

さて、EBNFで今更気が付いたけど要は”…”と/…/opt で表現できる文字列のみでテキストが出来ているので、一気に字句解析させてみようと思ったけど、

sequce( “{” , /.*/ , “}” ) を見ただけで、意味が無いことが判った。

どちらかと云えば、「空白文字の列」と「空白以外の文字の列」に分けて下ごしらえした方がマシ。

パーサのソースが巨大になってきたので、

  • パーサ
  • EBNF構文のパーサ
  • 数式構文のパーサ

に分離した。

wikiのEBNFの内容が充実してきたので、EBNF部分を見直してみると、グループやオプションでも「*」が使える様な雰囲気なので、EBNFのテキストを変えてみたら、rule-listすら通らない!

ENFテキスト中の “{” … “}”と違い、{ … } はEBNFのrepeateパーサを直接呼び出しているのにやっと気づき、EBNFのrepeate、group、optionパーサの文法も修正したので暫くは持ちそう。

(*$ SEQUENCE SEPARATOR IS REQUIRED $*)
(* 文法   *)
syntax      =   rule list, /\s*/ ;
rule list   =   { ( rule | special effect | comment ) } + ;
rule        =   identifier, defining symbol, definitions, ";" ;
defining symbol = "=" | "::=" ;                                     (* 終端記号 '=':EBNF, '::=':BNF *)
(* 定義   *)
definitions =   definition, { "|", definition } ;                   (* 定義リスト *)
definition  =   choice ;                                            (* 定義 *)
(* リスト *)
choice      =   sequence, { "|", sequence} ;                        (* アレかコレかソレかのどれか *)
sequence    =   exception, { ",", exception } ;                     (* アレとコレとソレの一式。","は空白改行と同義  *)
exception   =   words, { "-", words } ;                             (* アレが良いが、コレとソレを除く。 *)
(* 囲み   *)
words      =   [ comments ], word, [ comments ] ;
special effect =  (*$ NO SPACE SKIP $*) "(*$", /[^\\?]*/, "$*)" ;   (* 特殊効果 *)
comments    =   ( special effect | comment ), repeat symbol;
word        =   ( sq string | wq string | wraper | special text | regexp | identifier ) ;
comment     =   (*$ NO SPACE SKIP $*) "(*", /(?![*][/])(.*)/, "*)" ; (* 注意書き *)
sq string   =   (*$ NO SPACE SKIP $*) "'", /(?!\\\\)[^']*/, "'" ;   (* ' で括られたテキスト *)
wq string   =   (*$ NO SPACE SKIP $*) '"', /(?!\\\\)[^"]*/, '"' ;   (* " で括られたテキスト *)
wraper      =   ( group | option | repeate ), repeat symbol ;
group       =   "(", choice, ")" ;                                  (* choiceを明示的に括る表現 *)
option      =   "[", choice, "]" ;                                  (* あれば尚良し *)
repeate     =   "{", sequence, "}" ;                                (* 0回以上繰り返す *: 0回以上、+:1回以上 *)
regexp      =   /.+/i ;                                             (* 正規表現されたテキスト "/"  + /.+/ "/" + { /[dgimsuvy]/ } *)
identifier  =   /[A-Za-z][A-Z_a-z0-9\\x20]*/;                       (* 識別子 *)
special text =  (*$ NO SPACE SKIP $*) "?", /[^\\?]*/, "?" ;         (* 特殊文字列 *)
repeat symbol=  [ "*" | "+" | '{', [ number ], ',', [ number ], '}']; (* 繰返し記号 *)
number      =   /\d+/ ;

EBNFは定義記号が=なので=でも::=でも通る様に修正、未終端記号(識別子のことか?)に空白を挟めるみたいだが、そうなると、sequenceのセパレータを必須にしなければいけなくなるので、

? NO SPACE SKIP ? (sequence内とその配下で有効)の他に特殊文字列を追加し、

? SEQUENCE SEPARATOR IS (NOT) REQUIRED? 指定で、識別子に空白OK(NO)にすれば良いかな?

ps.2024/4/8

やっと数値を右揃えで表示。論理式は数式のみ。’xxxxxで文字列をEBNFに追記。

? SEQUENCE SEPARATOR IS (NOT) REQUIRED?の方は組み込んでみた。

実装の都合上、branchパーサを追加

/**
  * 判定式の結果で処理を分岐するパーサを作成する
  * @param {Function} expr           判定式
  * @param {Function} trueParser     結果が真の場合に実行するパーサ
  * @param {Function} falseParser    結果が偽の場合実行するパーサ
  * @returns 
  */
 branch(expr, trueParser, falseParser) {
     const methodName = 'branch';
     trueParser = this.normalizeParses(trueParser);
     falseParser = this.normalizeParses(falseParser);
     this.chkFunction(methodName, "trueParser", trueParser);
     this.chkFunction(methodName, "falseParser", falseParser);
     const func =
         /**
          *  生成した処理を分岐するパーサ
          * @param {string} target       ソースコード
          * @param {integer} position    読取り位置
          * @return {ParseInfo}          パースした結果
          */
         (target, position) => {/*branch*/
             if (expr()) {
                 return trueParser(target, position);
             } else {
                 return falseParser(target, position);
             }
         }
     return func;
 }
const identifier = _map(
    _branch(() => { return this.sequnceSeparator }, /[A-Za-z][-A-Z_a-z0-9\x20]*/, /[A-Za-z][-A-Z_a-z0-9]*/)
    , (parsed) => {
        parsed = parsed.trim();
        parsed = parsed.replaceAll(/\x20{2,}/g, '\x20');   //空白が並んでいた場合は1つにまとめる
        return { '#identifier': parsed };
    });
const sequnce = _map(
    wraper, _repeate(_branch(() => { return this.sequnceSeparator }, ",", _option(",")), wraper, 0)
    , (parsed) => {
        const methodName = "sequnceのfn";
            ・・・

この程度の処理でもmapのsequenceパラメータ部に分岐処理を詰め込む必要があったのでパーサにしないといけないが、このままでは「?特殊文字列?」の機能はEbnfParser限定になってしまう。後、?と?の間の空白は両端をTrimし連続する空白は1個にまとめている。

?テスト?

サンプル = ?=テスト?, "TEST", "NOW" | "TEST" ;    ☚上で?テスト?を宣言すると?=テスト?がtrue,'テスト'を返すとか?

のような使い方ができると分岐も書けて便利かな?特殊文字をコメントっぽく扱っているので、ちゃんと文字列あるいは何かのリテラルとして扱える様にしないとダメかもしれない。EBNFの*演算子は正規表現っぽく使わないらしいのでこの辺を見直さないといけないかな?例外’ー’の意味が判らなかったが、集合の差らしい。つまり

集合の積 = A , B , C ;        (* A 且つ、 B 且つ、 C である *)
集合の和 = A | B | C ;        (* A または、B または、C である *)
集合の差 = A - B - C ;        (* A である、B は除外、C も除外 *)

っぽい考えらしい。実際には EBNFでは集合の積より順序の方が意味が強い、また最後のは、(* A である、B は除外、でも C は含む *)とも読めるけど、面倒そうだから気のせいにして、実装した。

/**
 *  例外(集合の差)パーサを作成する
 * @param {Array of parser} parsers     例外(集合の差)するパーサの配列
 * @return {Function} 					生成した連結パーサ
 */
exception(...parsers) {
    const methodName = 'exception';
    // パーサに正規表現やテキストが混ざっていたらパーサに変換する
    parsers = this.normalizeParses(parsers);
    this.chkFunction(methodName, "parsers", parsers);
    // パーサが単体の場合はそのまま返す
    if (parsers.length === 1) {
        this.chkFunction(methodName, "parsers[0]", parsers[0]);
        return parsers[0];
    }
    // パーサが複数の場合は、全ての判定が真の場合に成功を返すパーサを渡す
    const func =
        /**
         *  生成した例外(集合の差)パーサ
         * @param {string} target       ソースコード
         * @param {integer} position    読取り位置
         * @return {ParseInfo}          パースした結果
         */
        (target, position) => {/*sequnce*/
            const bk = this.fSpaceSkip;
            if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
            const oldPosition = position;
            let result;
            for (let i = 0; i < parsers.length; i++) {
                const parser = parsers[i];
                this.chkFunction(methodName, "parser", parser);
                const parsed = parser(target, position);
                if (parsed === undefined) {
                    continue;
                }
                if ((i === 0 && parsed.success) || (i !== 0 && !parsed.success)) {
                    if (i === 0) {
                        result = parsed.result;
                    }
                } else {
                    parsed.position = oldPosition;
                    // 一つでも判定で偽となれば、このパーサ自体が失敗を返す
                    parsed.success = false;
                    this.fSpaceSkip = bk;
                    return parsed;
                }
            }
            this.fSpaceSkip = bk;
            return new ParseInfo(true, result, position);
        };
    return func;
}

sequenceと違うのは名前とtrue,falseの判定の仕方だけ

「-」を演算子に使うので非終端記号(識別子)に「-」は使えなくなったのが地味に痛い。ここにもあっちにも「-」が散乱してたので修正ががが。

セルの数式をトークン化した影響でJSONファイルをアップロードするとセルに数式が入らない、日付がテキストに化けていたのを修正。テキストファイルのアップロードも。

ps.2024/4/11

数式のセル参照がセル範囲形式の場合に先頭と最終セルを格納していたので中間のセルの値が変わっても反応していなかった。マクロとJDOCと正規表現のEBNF構文を模索中。

  • 今も正規表現を正規表現でちゃんと表現していないので時々通らない事がある
    • とりあえずEBNF化して解析できるようにしたい。
  • javascriptっぽいマクロが書けるようにしたい
    • 組み込み関数もマクロで書いた方が楽そうだから
      • でも、全く型無し支援無しは辛いのでJDOCをマクロ構文に盛り込みたい
        • JDOCもEBNF化したい
          • このマクロのEBNFを1つのBNFで書けない事はないがコピペだらけになる
            • 各々のEbnfParser派生クラスで作成したparserを参照できるようにしたい。
              • 作成する順番に依存すると大変
                • lazy的な参照の仕組みにしよう
  • EbnfPaser ,ExprParser, ( MacroParser, JdocParser }
    • RegexpParserは最終結果をEbnfParserのparser.regexpのパターンに組込む
      • &:EBNFのパターンを空白無しで処理する演算子に正規表現も組み込めそう
        • “/” + /.*/ + “/” 的なものが予想通りな結果になったら嬉しい
  • パーサ作成部(makeParser)とパーサ実行部(parse)に分離
    • 各々のコンストラクタで自分の構文を事前にパーサ作成させる
      • makeParserとparserは各々継承させる

ps.2024/4/12

パーサ作成処理内でテストパターンを処理させるようにしたら、ほとんどの数式がテキスト扱い。数式のEBNFはあちこちに*を付けていたが目障りで何となく削除したせいだった。真の原因は肝心の{…}のパーサの処理がリピートしておらず実質choiceになっていて、ごく一部の2つのパラメータを持つ関数や1+2などは数式として扱っていたものの、=1とかはテキストになっていた。関数のパラメータが無い場合の扱いが曖昧だった。

(*$ SEQUENCE SEPARATOR IS NOT REQUIRED $*)
(* セルのデータ *)
cell                = expr_cell | value_cell ;
(* 値 *)
value_cell          = time_stamp | date | time | number | boolean | sqstring | text_etc ;
time_stamp          = date ' ' time ;
date                = yyyy '/' MM '/' dd  | MM '/' dd ;
yyyy                = /\\d{4}/ ;
MM                  = /\\d{1,2}/ ;
dd                  = /\\d{1,2}/ ;
time                = HH ':' mm ':' ss | HH ':' mm  ;
HH                  = /\\d{1,2}/ ;
mm                  = /\\d{1,2}/ ;
ss                  = /\\d{1,2}/ ;
number              = /[-]?([0-9]+)([.][0-9]*)?/ ;
boolean             = /true/i | /false/i ;
sqstring            = /'.*/ ;
text_etc            = /.+/ ;
(* 数式 *)
expr_cell           = '=' expr ;
expr                = logical_expr ;
logical_expr        = add_sub_expr [ ( '=' | '>' | '<' | '>=' | '<=' | '<>' ) add_sub_expr ] ;
add_sub_expr        = mul_div_expr { ( '+' | '-' | '&' ) mul_div_expr } ;
mul_div_expr        = factor { ( '*' | '/' ) factor } ;
factor              = value | parenthesis_expr ;
parenthesis_expr    = '(' expr ')' ;
value               = number | boolean | function_def | a1_range | a1 | dqstring ;
a1_range            = a1 ':' a1 ;
a1                  = /([A-Za-z]+[1-9][0-9]*)/ ;
dqstring            = /"[^"]*"/ ;
function_def        = symbol '(' parameters ')' ;
symbol              = /([A-Za-z][A-Za-z0-9_]*)/ ;
parameters          = [ expr ] { ',' expr } ;

それにしても、さっきまでparameters = expr { ‘,’ expr } ;で動いていたのはなぜなんだろう。

  • 循環計算をescキー入力で中断させたい。
    • 数式セルに計算回数(count)を追加し、どこかでセルの内容を手入力で書き換えたらリセットして再計算を開始、セル値が更新したら+1し、10,100,1000,10000になったら、中止確認メッセージ
    • どこかでセルの内容を手入力で書き換えたら、再計算開始時刻をリセットし、10秒たっても再計算が完了しなかったら、「タイムオーバー」

ps.2024/4/13

繰返し演算子に正規表現の{n,m}を追加したところ、パーサが長く読みにくいので、呼出部(parser.js)は今まで通りにパラメータの調整やチェックの後、基本部(coreParser.js)で生成されたパーサに空白読飛し等をラップする。

・どうやってもログのクラス名(this.className)が派生クラス名になる。

ps.2024/4/14

EBNFのパーサを微調整。repeat symboleを囲み文字やコメントのみにした。継承クラス同士で同じメソッド名になるがどうしても自分のクラスのメソッドを呼びたいケースがあるのでローカルメソッド名(#xxxx)を併用。

xxxx(a,b,c) { // 継承先のクラスのメソッドを呼びたい場合に使用する
  return this.#xxxx(a,b,c);
}
#xxxx(a,b,c) { // 自分のクラスのメソッドをどうしても呼びたい場合に使用する ※makeParser, parse, selfTest, etc.
 本体
}

ps.2024/4/15

でも、全ソースで同じ内容の#メソッドを書くのは、後々面倒なので、

・・・各派生クラスのconstructorに以下をコピペして
this.selfTest(
    this.#getSelfTestList(), this.#parser,
    {
        className: 'EbnfParser',
        testLog: false, jsonLog: false, mapLog: false,
        parse: this.#parse.bind(this)
    }
);
と呼び出し、

・・・以下、基底クラスで、実行させる
/**
 * 作成したパーサをテストする
 * @param {Function} parser 
 * @param {string} testText 
 * @param {object} info 
 * @returns 
 */
selfTest(testText, parser, info) {
    const methodName = 'selfTest';
    if (Array.isArray(testText)) {
        const stack = testText.map((t, index, a) => {
            if (info) {
                console.log(`${info.className}.${methodName}: pattern[${index + 1}/${a.length}]`);
            }
            return this.selfTest(t, parser, info);
        });
        return stack;
    } else {
        if (info && info.testLog) {
            this.map.DEBUG = info.mapLog;
            console.log(`${info.className}.${methodName}: text:'${testText}'`);
        }
        const result = info.parse(testText, parser);
        if (info.jsonLog) {
            this.jsonStringifyLog(result);
        }
        return result;
    }
}

と、ローカルなメソッドをパラメータに列挙してかいくぐることにした。※いつかシンタックスなエラーになりそうだけど。

ps.2024/4/16

EBNFテキストはセルフサンプル用なので色々変更しても、数式など他のEBNF文法に影響を与えないので、どうでもいいのだけれど・・・EBNFテキストで「repeate='{‘, sequence, ‘}’」と修正した。実際のEBNFパーサrepeateの実装に揃えた形。なぜsequenceになったのか?文法の繰り返しで多用する「'{‘ 演算子, 非終端記号 ‘}’」を書くたびにrepeate(sequence(a,b,c}}と毎度sequenceを書くのが面倒で、多用する方に実装方法を寄せました。

それよりも

記事中のsequnceに波線が付いていたけど・・・sequence not found.のエラーメッセージの影響で、EBNFのsequenceの綴りを訂正。いっぱい間違っていたので、修正漏れとか修正ミスとかでサッパリ動かず。

&演算子の結果がundefinedだった。式が未入力のセルを参照した場合は数値の0として扱う。



[javascript]spread-sheetカスタムエレメントその2

EBNF解析用に書き換えた組み込みパーサをスプレッドシートに移植できたので、次はスプレッドシートの数式の文法をENBF風に書いてみると

(* セルのデータ *)
cell                ::= expr_cell | value_cell ;
(* 値 *)
value_cell          ::= time_stamp | date | time | logical_expr | number | boolean | text_etc ;
time_stamp          ::= date ' ' time ;
date                ::= yyyy '/' MM '/' dd  | MM '/' dd ;
yyyy                ::= /\\d{4}/ ;
MM                  ::= /\\d{1,2}/ ;
dd                  ::= /\\d{1,2}/ ;
time                ::= HH ':' mm ':' ss | HH ':' mm  ;
HH                  ::= /\\d{1,2}/ ;
mm                  ::= /\\d{1,2}/ ;
ss                  ::= /\\d{1,2}/ ;
number              ::= /[-]?([0-9]+)([.][0-9]*)?/ ;
boolean             ::= /true/i | /false/i ;
text_etc            ::= /.+/ ;
(* 数式 *)
expr_cell           ::= '=' expr ;
expr                ::= logical_expr ;
logical_expr        ::= add_sub_expr [ ( '=' | '>' | '<' | '>=' | '<=' | '<>' ) add_sub_expr ] ;
add_sub_expr        ::= mul_div_expr { ( '+' | '-' | '&' ) mul_div_expr }* ;
mul_div_expr        ::= factor { ( '*' | '/' ) factor } ;
factor              ::= value | parenthesis_expr ;
parenthesis_expr    ::= '(' expr ')' ;
value               ::= number | boolean |  function_def | a1_range | a1 | dqstring ;
a1_range            ::= a1 ':' a1 ;
a1                  ::= /([A-Za-z]+[1-9][0-9]*)/ ;
dqstring            ::= /".*"/ ;
function_def        ::= symbol '(' parameters ')' ;
symbol              ::= /([A-Za-z][A-Za-z0-9_]*)/ ;
parameters          ::= expr { ',' expr } ;

ここでoption( […])の中身をsequenceとしているので、EBNF解析パーサもchoiceからlistに変更してドッチでも良しのつもりだったが、”(…) 値 ”の様な場合は、(…)でchoice様に当確判定が出てしまい(値)ではなく即(])判定に走ってミスとなったので、sequenceに変えた。※EBNFのlist を sequence | choice; に変えようかなとも思ったがchoiceが一切通らなくなる様な気がする。

よくよく考えてみると文法の定義(defintion)文はsequenceが基本で、文法上sequenceを明示する書式がなかったりする。「,」で区切れば可能だが、何かありそう。

{
  "cell": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "expr_cell" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "value_cell" }
          ]
        }]
    }}},
{
  "value_cell": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "time_stamp" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "date" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "time" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "logical_expr" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "number" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "boolean" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "text_etc" }
          ]
        }]
    }}},
{
  "time_stamp": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "date" },
            { "_sq_string": " " },
            { "_identifier": "time" }
          ]
        }]
    }}},
{
  "date": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "yyyy" },
            { "_sq_string": "/" },
            { "_identifier": "MM" },
            { "_sq_string": "/" },
            { "_identifier": "dd" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "MM" },
            { "_sq_string": "/" },
            { "_identifier": "dd" }
          ]
        }]
    }}},
{
  "yyyy": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/\\d{4}/" }
          ]
        }]
    }}},
{
  "MM": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/\\d{1,2}/" }
          ]
        }]
    }}},
{
  "dd": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/\\d{1,2}/" }
          ]
        }]
    }}},
{
  "time": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "HH" },
            { "_sq_string": ":" },
            { "_identifier": "mm" },
            { "_sq_string": ":" },
            { "_identifier": "ss" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "HH" },
            { "_sq_string": ":" },
            { "_identifier": "mm" }
          ]
        }]
    }}},
{
  "HH": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/\\d{1,2}/" }
          ]
        }]
    }}},
{
  "mm": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/\\d{1,2}/" }
          ]
        }]
    }}},
{
  "ss": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/\\d{1,2}/" }
          ]
        }]
    }}},
{
  "number": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/[-]?([0-9]+)([.][0-9]*)?/" }
          ]
        }]
    }}},
{
  "boolean": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/true/i" }
          ]
        },
        {
          "_sequence": [
            { "_regexp": "/false/i" }
          ]
        }]
    }}},
{
  "text_etc": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/.+/" }
          ]
        }]
    }}},
{
  "expr_cell": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_sq_string": "=" },
            { "_identifier": "expr" }
          ]
        }]
    }}},
{
  "expr": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "logical_expr" }
          ]
        }]
    }}},
{
  "logical_expr": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "add_sub_expr" },
            {
              "_option": [
                {
                  "_sequence": [
                    {
                      "_group": [
                        {
                          "_sequence": [
                            { "_sq_string": "=" }
                          ]
                        },
                        {
                          "_sequence": [
                            { "_sq_string": ">" }
                          ]
                        },
                        {
                          "_sequence": [
                            { "_sq_string": "<" }
                          ]
                        },
                        {
                          "_sequence": [
                            { "_sq_string": ">=" }
                          ]
                        },
                        {
                          "_sequence": [
                            { "_sq_string": "<=" }
                          ]
                        },
                        {
                          "_sequence": [
                            { "_sq_string": "<>" }
                          ]
                        }]
                    },
                    { "_identifier": "add_sub_expr" }
                  ]
                }]
            }]
        }]
    }}},
{
  "add_sub_expr": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "mul_div_expr" },
            {
              "_repeate0": [
                {
                  "_sequence": [
                    {
                      "_group": [
                        {
                          "_sequence": [
                            { "_sq_string": "+" }
                          ]
                        },
                        {
                          "_sequence": [
                            { "_sq_string": "-" }
                          ]
                        },
                        {
                          "_sequence": [
                            { "_sq_string": "&" }
                          ]
                        }]
                    },
                    { "_identifier": "mul_div_expr" }
                  ]
                }]
            }]
        }]
    }}},
{
  "mul_div_expr": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "factor" },
            {
              "_repeate0": [
                {
                  "_sequence": [
                    {
                      "_group": [
                        {
                          "_sequence": [
                            { "_sq_string": "*" }
                          ]
                        },
                        {
                          "_sequence": [
                            { "_sq_string": "/" }
                          ]
                        }]
                    },
                    { "_identifier": "factor" }
                  ]
                }]
            }]
        }]
    }}},
{
  "factor": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "value" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "parenthesis_expr" }
          ]
        }]
    }}},
{
  "parenthesis_expr": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_sq_string": "(" },
            { "_identifier": "expr" },
            { "_sq_string": ")" }
          ]
        }]
    }}},
{
  "value": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "number" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "boolean" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "function_def" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "a1_range" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "a1" }
          ]
        },
        {
          "_sequence": [
            { "_identifier": "dqstring" }
          ]
        }]
    }}},
{
  "a1_range": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "a1" },
            { "_sq_string": ":" },
            { "_identifier": "a1" }
          ]
        }]
    }}},
{
  "a1": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/([A-Za-z]+[1-9][0-9]*)/" }
          ]
        }]
    }}},
{
  "dqstring": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/\".*\"/" }
          ]
        }]
    }}},
{
  "function_def": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "symbol" },
            { "_sq_string": "(" },
            { "_identifier": "parameters" },
            { "_sq_string": ")" }
          ]
        }]
    }}},
{
  "symbol": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_regexp": "/([A-Za-z][A-Za-z0-9_]*)/" }
          ]
        }]
    }}},
{
  "parameters": {
    "_definition": {
      "_choice": [
        {
          "_sequence": [
            { "_identifier": "expr" },
            {
              "_repeate0": [
                {
                  "_sequence": [
                    { "_sq_string": "," },
                    { "_identifier": "expr" }
                  ]
                }]
            }]
        }]
    }}}

「パーサコンビネーションでの実装を諦める」方向に進みそうな雰囲気になってきたが、パーサコンビネーションの方がデバッグしやすいと云う妄想が消え去った後ではコレで良いのかもしれない。

正規表現のオプションの記述が抜けている方が気になるが、空白や改行は基本的に無視する数式文法なので問題は無いだろう。

完璧なテストパターンが欲しくなってきたが、まだまだ内部実装は変わりそうなので諦め。



[jabascript]EBNF

BNFがまぁまぁできたので、EBNFにチャレンジ。

syntax = syntax_rule { "|" syntax_rule } ;
syntax_rule = identifier "=" definitions_list ";" ;
definitions_list = single_definition { "|" single_definition } ;
single_definition = term { term } ;
term = [ repetition ] ( group | option | literal | nonterminal ) ;
repetition = "{" | "}" | "[" | "]" ;
group = "(" definitions_list ")" ;
option = "[" definitions_list "]" ;
literal = "'" character "'" | '"' character '"' ;
nonterminal = identifier ;
identifier = letter { letter | digit | "_" } ;
letter = "A" | "B" | ... | "Z" | "a" | "b" | ... | "z" ;
digit = "0" | "1" | ... | "9" ;
character = letter | digit | symbol ;
symbol = "$" | "&" | "#" | "@" | "%" | "+" | "-" | "/" | "*" | "!" | "?" | "<" | ">" | "=" | ":" | "~" | "^" | "." | "|" ;

そのまま、コード化してもうまくいかず・・・

(* 文法   *)
syntax      =   rule_list /\s*/ ;
rule_list   =   { ( rule | comment ) }+;
rule        =   identifier ( "=" | "::=" ) definitions ";";
(* 定義   *)
definitions =   definition { "|" definition }* ;                (* 定義リスト *)
definition  =   { ( list | wrap | word ) }+ ;                   (* 定義 *)
(* リスト *)
list        =   choice | sequnce ;                              (* 二項演算子でwrapをr繋いだリスト *)
choice      =   ( wrap | word ) { "|" ( wrap | word ) }* ;      (* アレかコレかソレかのどれか *)
sequnce     =   ( wrap | word ) { [ "," ] ( wrap | word ) }* ;  (* アレとコレとソレの一式。","は空白改行と同義  *)
(* 囲み   *)
wrap        =   ( option | group | repeate );
option      =   "[" choice "]" ;                                (* あれば尚良し *)
group       =   "(" choice ")" ;                                (* choiceを明示的に括る表現 *)
repeate     =   "{" sequnce "}" [ "*" | "+" ] ;                 (* 0回以上繰り返す *: 0回以上、+:1回以上 *)
(* 語彙   *)
word        =   ( wq_string | sq_string | regexp | identifier ) ;       
wq_string   =   '"' /(?!\\)[^"]*/ '"' ;                               (* " で括られたテキスト *)
sq_string   =   "'" /(?!\\)[^']*/ "'" ;                               (* ' で括られたテキスト *)
regexp      =   "/" /.+/ "/" ;                                  (* 正規表現されたテキスト *)
identifier  =   /[A-Za-z][A-Za-z0-9_]*/ ;                       (* 名称 *)
comment     =   /[(][*](.*)[*][)]/ ;                            (* 注意書き *)

に変更した。

ちょっと使いにくいのが

repeate     =   "{" sequnce "}" { repeate_opt } ; 

で、例えば、

{ a | b } aかbがあれば良いなぁ~
{ a b }  a b と並ぶと良いなぁ~

どちらも使えたら良いと思えるけど、パーサジェネレータにコードする際に

//  repeate     =   "{" sequnce "}" { repeate_opt } ;
const repeate = _lazy(() => {
    return _map(
        "{", sequnce, "}", _option(repeate_opt)
        , (parsed) => {
            return { rule: parsed };
        })
});

な感じでEBNF文法を取り込む時に

{ a, b, c, d } ⇒ _repeate( a, b, c, d )

のパラメータ部をsequnceとして解釈すれば、ほぼ書き直さずに済むけど

{ a | b | c | d } ⇒ _repeate( a | b | c | d )

のパラメータ部をgroupとして解釈できなくもないが、a b c dの戻り値がboolean型に制限される。

つまり、パーサコンビネータが{ true: 成功、false: 失敗 }しか返せなくなってしまう。

文法の解釈した結果を外部変数にスタックし自分が読込んだ位置を保持すれば済む話ではあるけれど、面倒この上無いし、そこまで整えるくならLL法のパーサライブラリィを使えば良い気がする。

{
   "_syntax": {
     "_rule_list": [
       { "_comment": "(* 文法  *)" },
       {
         "syntax": {
           "_definition": {
             "_sequence": [
               { "_identifier": "rule_list" },
               { "_regexp": "/s*/" }
             ]
           }
         }
       },
       {
         "rule_list": {
           "_definition": {
             "_repeate1": {
               "_group": [
                 { "_identifier": "rule" },
                 { "_identifier": "comment" }
               ]
             }
           }
         }
       },
       {
         "rule": {
           "_definition": {
             "_sequence": [
               { "_identifier": "identifier" },
               {
                 "_group": [
                   { "_wq_string": "=" },
                   { "_wq_string": "::=" }
                 ]
               },
               { "_identifier": "definitions" },
               { "_wq_string": ";" }
             ]
           }
         }
       },
       { "_comment": "(* 定義  *)" },
       {
         "definitions": {
           "_definition": {
             "_sequence": [
               { "_identifier": "definition" },
               {
                 "_repeate0": {
                   "_sequence": [
                     { "_wq_string": "|" },
                     { "_identifier": "definition" }
                   ]
                 }
               }
             ]
           }
         }
       },
       { "_comment": "(* 定義リスト *)" },
       {
         "definition": {
           "_definition": {
             "_repeate1": {
               "_group": [
                 { "_identifier": "list" },
                 { "_identifier": "wrap" },
                 { "_identifier": "word" }
               ]
             }
           }
         }
       },
       { "_comment": "(* 定義 *)" },
       { "_comment": "(* リスト *)" },
       {
         "list": {
           "_definition": {
             "_choice": [
               { "_identifier": "choice" },
               { "_identifier": "sequnce" }
             ]
           }
         }
       },
       { "_comment": "(* 二項演算子でwrapをr繋いだリスト *)" },
       {
         "choice": {
           "_definition": {
             "_sequence": [
               {
                 "_group": [
                   { "_identifier": "wrap" },
                   { "_identifier": "word" }
                 ]
               },
               {
                 "_repeate0": {
                   "_sequence": [
                     { "_wq_string": "|" },
                     {
                       "_group": [
                         { "_identifier": "wrap" },
                         { "_identifier": "word" }
                       ]
                     }
                   ]
                 }
               }
             ]
           }
         }
       },
       { "_comment": "(* アレかコレかソレかのどれか *)" },
       {
         "sequnce": {
           "_definition": {
             "_sequence": [
               {
                 "_group": [
                   { "_identifier": "wrap" },
                   { "_identifier": "word" }
                 ]
               },
               {
                 "_repeate0": {
                   "_sequence": [
                     {
                       "_option": { "_wq_string": "," }
                     },
                     {
                       "_group": [
                         { "_identifier": "wrap" },
                         { "_identifier": "word" }
                       ]
                     }
                   ]
                 }
               }
             ]
           }
         }
       },
       { "_comment": "(* アレとコレとソレの一式。\",\"は空白改行と同義 *)" },
       { "_comment": "(* 囲み  *)" },
       {
         "wrap": {
           "_definition": {
             "_group": [
               { "_identifier": "option" },
               { "_identifier": "group" },
               { "_identifier": "repeate" }
             ]
           }
         }
       },
       {
         "option": {
           "_definition": {
             "_sequence": [
               { "_wq_string": "[" },
               { "_identifier": "choice" },
               { "_wq_string": "]" }
             ]
           }
         }
       },
       { "_comment": "(* あれば尚良し *)" },
       {
         "group": {
           "_definition": {
             "_sequence": [
               { "_wq_string": "(" },
               { "_identifier": "choice" },
               { "_wq_string": ")" }
             ]
           }
         }
       },
       { "_comment": "(* choiceを明示的に括る表現 *)" },
       {
         "repeate": {
           "_definition": {
             "_sequence": [
               { "_wq_string": "{" },
               { "_identifier": "sequnce" },
               { "_wq_string": "}" },
               {
                 "_option": {
                   "_choice": [
                     { "_wq_string": "*" },
                     { "_wq_string": "+" }
                   ]
                 }
               }
             ]
           }
         }
       },
       { "_comment": "(* 0回以上繰り返す *: 0回以上、+:1回以上 *)" },
       { "_comment": "(* 語彙  *)" },
       {
         "word": {
           "_definition": {
             "_group": [
               { "_identifier": "wq_string" },
               { "_identifier": "sq_string" },
               { "_identifier": "regexp" },
               { "_identifier": "identifier" }
             ]
           }
         }
       },
       {
         "wq_string": {
           "_definition": {
             "_sequence": [
               { "_sq_string": "\"" },
               { "_regexp": "/(?!\\)[^\"]*/" },
               { "_sq_string": "\"" }
             ]
           }
         }
       },
       { "_comment": "(* \" で括られたテキスト *)" },
       {
         "sq_string": {
           "_definition": {
             "_sequence": [
               { "_wq_string": "'" },
               { "_regexp": "/(?!\\)[^']*/" },
               { "_wq_string": "'" }
             ]
           }
         }
       },
       { "_comment": "(* ' で括られたテキスト *)" },
       {
         "regexp": {
           "_definition": {
             "_sequence": [
               { "_wq_string": "/" },
               { "_regexp": "/.+/" },
               { "_wq_string": "/" },
               {
                 "_repeate0": { "_regexp": "/[dgimsuvy]/" }
               }
             ]
           }
         }
       },
       { "_comment": "(* 正規表現されたテキスト *)" },
       {
         "identifier": {
           "_definition": { "_regexp": "/[A-Za-z][A-Za-z0-9_]*/" }
         }
       },
       { "_comment": "(* 名称 *)" },
       {
         "comment": {
           "_definition": { "_regexp": "/[(][*](.*)[*][)]/" }
         }
       },
       { "_comment": "(* 注意書き *)" }
     ]
   }
 }
 

と結果が得られたので良し。エディタを使って短くするのが面倒になってきたので

let json = JSON.stringify(list, null, 2);
// 特定のデータを一行に纏める呪文
json = json.replaceAll(/[{]\s*"(_identifier|_regexp|_wq_string|_sq_string|_comment)":\s*(".*")\s*[}]/g, '{ "$1": $2 }');
console.log(json);

一行呪文を生成した。やはり、【メンドクサイ】はプログラミングの母である。

class EBNF {
    /**
     *  EBNFルールと文法を作成
     * @param {string} ebnf      BNFテキスト
     * @param {function} proc   mapのproc
     * @returns mapまたはbnfパターンの戻り値
     */
    parseEBNF2(ebnf, proc) {
        const _map = this.map.bind(this);
        const _sequence = this.sequence.bind(this);
        const _option = this.option.bind(this);
        const _group = this.choice.bind(this);
        const _choice = this.choice.bind(this);
        const _repeate = this.repeate.bind(this);
        const _lazy = this.lazy.bind(this);
        //  sequnce, choice, repeateのパラメータのparser部に差し込んで使用する
        const _debugPrint = (target, position) => {
            const text = this.positionText(target, position);
            console.log(`_trap: '${text}'`);
            return undefined;
        };
        const _getValue = (a) => {
            const keys = Object.keys(a);
            return a[keys[0]];
        }
        //  comment     =   "(*" /(?![*][)])(.*)/ ;
        const comment = _map(
            /[(][*](.*)[*][)]/
            , (parsed) => {
                return { _comment: parsed };
            });
        //  identifier  =   /[A-Za-z][A-Za-z0-9_]*/ ;
        const identifier = _map(
            /[A-Za-z][A-Za-z0-9_]*/
            , (parsed) => {
                return { _identifier: parsed };
            });
        //  regexp      =   "/" /(?!\\)[^/]+/ "/" ;
        const regexp = _map(
            "/", /(?!\\)[^/]+/, "/"
            , (parsed) => {
                return { _regexp: `/${parsed[1]}/` };
            });
        //  sq_string   =   "'" /(?!\\)[^"]*/ "'" ;
        const sq_string = _map(
            "'", /(?!\\)[^']*/, "'"
            , (parsed) => {
                return { _sq_string: parsed[1] };
            });
        //  wq_string   =   '"' /(?!\\)[^"]*/ '"' ;
        const wq_string = _map(
            '"', /(?!\\)[^"]*/, '"'
            , (parsed) => {
                return { _wq_string: parsed[1] };
            });
        //  word        =   wq_string | sq_string | regexp | identifier ;
        const word = _map(
            _choice(wq_string, sq_string, regexp, identifier)
            , (parsed) => {
                return parsed;
            });
        //  repeate     =   "{" sequnce "}" [ "*" | "+" ] ;
        const repeate = _lazy(() => {
            return _map(
                "{", sequnce, "}", _option("*", "+")
                , (parsed) => {
                    const lparen = parsed[0];
                    const seq = parsed[1];
                    const rparen = parsed[2];
                    const opt = parsed[3];
                    return (opt === '+') ? { _repeate1: seq } : { _repeate0: seq };
                })
        });
        //  group       =   "(" choice ")" ;
        const group = _lazy(() => {
            return _map(
                "(", choice, ")"
                , (parsed) => {
                    return { _group: parsed[1]._choice };
                })
        });
        //  option      =   "[" choice "]" ;
        const option = _lazy(() => {
            return _map(
                "[", choice, "]"
                , (parsed) => {
                    return { _option: parsed[1] };
                })
        });
        // wrap        =   ( option | group | repeate ) ;
        const wrap = _map(
            _group(option, group, repeate)
            , (parsed) => {
                if (Array.isArray(parsed)) {
                    if (parsed.length === 1) return parsed[0];
                    return { _wrap: parsed };
                }
                return parsed;
            });
        //  sequnce     =   ( wrap | word ) { [ "," ] ( wrap | word ) }* ;
        const sequnce = _map(
            _group(wrap, word), _repeate(_option(","), _group(wrap, word), 0)
            , (parsed) => {
                if (Array.isArray(parsed)) {
                    parsed = parsed.filter((p) => !(Array.isArray(p) && p.length === 0));
                    const stack = [];
                    let p1 = parsed.concat();
                    while (0 < p1.length) {
                        const p2 = p1.shift();
                        if (Array.isArray(p2)) {
                            p1 = p2.concat();
                        } else {
                            if (p2 != null) {
                                stack.push(p2);
                            }
                        }
                    }
                    //  配列要素が1個の場合はその内容を返す
                    if (stack.length === 1) {
                        return stack[0]
                    }
                    return { _sequence: stack };
                }
                return parsed;
            });
        //  choice      =   ( wrap | word ) { "|" ( wrap | word ) }* ;
        const choice = _map(
            _group(wrap, word), _repeate("|", _group(wrap, word), 0)
            , (parsed) => {
                if (Array.isArray(parsed)) {
                    parsed = parsed.filter((p) => !(Array.isArray(p) && p.length === 0));
                    if (parsed.length === 1) return parsed[0];
                    const stack = [];
                    const p1 = parsed.shift();
                    stack.push(p1);
                    let p2 = parsed.shift();
                    while (0 < p2.length) {
                        const p2a = p2.shift();
                        const p2aa = p2a.shift();
                        const p2ab = p2a.shift();
                        stack.push(p2ab);
                    }
                    return { _choice: stack };
                }
                return parsed;
            });
        //  list        =   choice | sequnce ;
        const list = _map(
            _choice(choice, sequnce)
            , (parsed) => {
                if (Array.isArray(parsed)) {
                    if (parsed.length === 1) return parsed[0];
                    return { _list: parsed };
                }
                return parsed;
            });
        //  definition  =   { ( list | wrap | word ) }+ ;
        const definition = _map(
            _repeate(_group(list, wrap, word), 1)
            , (parsed) => {
                if (Array.isArray(parsed)) {
                    //  _repeateが[]を返すっぽい
                    parsed = parsed.filter((p) => !(Array.isArray(p) && p.length === 0));
                    if (parsed.length === 1) return { _definition: parsed[0] };
                    return { _definition: { _sequence: parsed } };
                }
                return { _definition: parsed };
            });
        //  definitions =   definition { "|" definition }* ;
        const definitions = _map(
            definition, _repeate("|", definition, 0)
            , (parsed) => {
                if (Array.isArray(parsed)) {
                    //  _repeateが[]を返すっぽい
                    parsed = parsed.filter((p) => !(Array.isArray(p) && p.length === 0));
                }
                parsed.map((p) => p._definition);
                return parsed[0];
            });
        //  rule        =   identifier ( "=" | "::=" ) definitions ";";
        const rule = _map(
            identifier, _group("=", "::="), definitions, ";"
            , (parsed) => {
                const name = parsed[0]._identifier;
                const eq = parsed[1];
                const definitions = parsed[2];
                const semicolon = parsed[3];
                const a = {};
                a[name] = definitions;
                return a;
            });
        //  rule_list   =   { ( rule | comment ) }+ ;
        const rule_list = _map(
            _repeate(_group(rule, comment), 1)
            , (parsed) => {
                if (Array.isArray(parsed)) {
                    //  _repeateが[]を返すっぽい
                    parsed = parsed.filter((p) => !(Array.isArray(p) && p.length === 0));
                }
                return { _rule_list: parsed };
            });
        //  syntax      =   rule_list /\s*/ ;
        const syntax = _map(
            rule_list, /\s*/
            , (parsed) => {
                return { _syntax: parsed[0] };
            });
        //
        //  読取り位置の空白読み飛ばしフラグ
        this.fSpaceSkip = true;
        const result = syntax(ebnf, 0);
        if (!result.success) {
            console.log('パース失敗');
        } else if (ebnf.length !== result.position) {
            const text = this.positionText(ebnf, result.position);
            console.log(`${ebnf.length}文字中の${1 + result.position}文字目 '${text}' でパース失敗`);
        }
        if (proc) {
            return proc(result);
        }
        return result.result;
    }
    /**
     * 
     */
    testParseEBNF2() {
        const ebnf = `
            (* 文法   *)
            syntax      =   rule_list /\s*/ ;
            rule_list   =   { ( rule | comment ) }+;
            rule        =   identifier ( "=" | "::=" ) definitions ";";
            (* 定義   *)
            definitions =   definition { "|" definition }* ;                (* 定義リスト *)
            definition  =   { ( list | wrap | word ) }+ ;                   (* 定義 *)
            (* リスト *)
            list        =   choice | sequnce ;                              (* 二項演算子でwrapをr繋いだリスト *)
            choice      =   ( wrap | word ) { "|" ( wrap | word ) }* ;      (* アレかコレかソレかのどれか *)
            sequnce     =   ( wrap | word ) { [ "," ] ( wrap | word ) }* ;  (* アレとコレとソレの一式。","は空白改行と同義  *)
            (* 囲み   *)
            wrap        =   ( option | group | repeate );
            option      =   "[" choice "]" ;                                (* あれば尚良し *)
            group       =   "(" choice ")" ;                                (* choiceを明示的に括る表現 *)
            repeate     =   "{" sequnce "}" [ "*" | "+" ] ;                 (* 0回以上繰り返す *: 0回以上、+:1回以上 *)
            (* 語彙   *)
            word        =   ( wq_string | sq_string | regexp | identifier ) ;       
            wq_string   =   '"' /(?!\\)[^"]*/ '"' ;                         (* " で括られたテキスト *)
            sq_string   =   "'" /(?!\\)[^']*/ "'" ;                         (* ' で括られたテキスト *)
            regexp      =   "/" /.+/  "/" { /[dgimsuvy]/ } ;                (* 正規表現されたテキスト *)
            identifier  =   /[A-Za-z][A-Za-z0-9_]*/ ;                       (* 名称 *)
            comment     =   /[(][*](.*)[*][)]/ ;                            (* 注意書き *)
`;
        this.parseEBNF2(ebnf, (parsed) => {
            const list = parsed.result;
            let json = JSON.stringify(list, null, 2);
            // 特定のデータを一行に纏める呪文
            json = json.replaceAll(/[{]\s*"(_identifier|_regexp|_wq_string|_sq_string|_comment)":\s*(".*")\s*[}]/g, '{ "$1": $2 }');
            console.log(json);
            // list._syntax._rule_list.forEach((v) => {
            //     if (v._comment === undefined) {
            //         const json = JSON.stringify(v, null, 2) + ',';
            //         console.log(json);
            //     }
            // });
        });

パーサの結果を見ると意味が重複していたり、

_definition: [
"aaa",
・・・
"zzz",
]

と、_definition直下の配列は_choice?_sequence?どっちか悩みそうなので_sequenceの配下した等、結構内容を調整している。但し、_sequenceと_choiceは同じ様に_repeateの結果をフラットにするハズだったが他の結果の調整の差異で食い違が出てしまった。_wq_stringや_sq_stringがparsed[1]を返すのはJSON.stringifyでテキスト化した時に読みにくいからで、色々実装したらparsed.join(”)になるかもしれない。そして、組込みパーサもEBNF用パーサを書きやすくするために改造したのでスプレッドシートには使えないかもしれない。

    /**
     * 
     * @param {*} target 
     * @param {*} position 
     * @returns 
     */
    positionText(target, position, offset = 20) {
        const text = target.substring(position - offset, position)
            + '[' + target.substring(position, position + 1) + ']'
            + target.substring(position + 1, position + offset)
        return text;
    }
・・・中略・・・
    /**
     *  読取り位置の空白の読飛ばし
     * @param {string} target 
     * @param {integer} position 
     * @returns {Array} [target, position]
     */
    skipSpace(target, position) {
        const result = (/^\s*/).exec(target.substring(position));
        if (result) {
            const len = result[0].length;
            return [target, position + len];
        }
        return [target, position];
    }
    /**
     * 指定テキストのパーサ(関数)を作成する
     * @param {string} text         指定テキスト
     * @return {function}           生成した指定テキストのパーサ
     */
    token(text) {
        const methodName = 'token';
        const len = text.length;
        const func =
            /**
             *  生成した指定テキストのパーサ
             * @param {string} target       ソースコード
             * @param {integer} position    読取り位置
             * @return {ParseInfo}             パースした結果
             */
            (target, position) => {/*token*/
                if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
                if (typeof target !== 'string') {
                    let a = 1;
                }
                if (target.substring(position, position + len) === text) {
                    //  読取りに成功した場合
                    return new ParseInfo(true, text, position + len);
                } else {
                    //  読取りに失敗した場合
                    return new ParseInfo(false, null, position);
                }
            };
        return func;
    }
    /**
     *  指定したパーサの処理を指定した回数以上繰り返すパーサ(関数)を作成する
     * @param {...parser or integer} parsersAndMin    指定したパーサ,   最小繰返し回数
     * @return {function}               生成した指定したパーサの繰り返すを表現するパーサ
     */
    repeate(...parsersAndMin) {
        const methodName = 'repeate';
        let min = parsersAndMin.pop();
        let parsers = parsersAndMin;
        if (isNaN(min)) {
            parsers.push(min);
            min = 0;
        }
        //  parserをsequnce化
        const parser = this.sequnce(...parsers);
        if (parser === null) { console.error(`${methodName}: parser is null.`); }
        if (parser === undefined) { console.error(`${methodName}: parser is undefined.`); }
        if (!parser instanceof Function) { console.error(`${methodName}: parser is not a function.`); }
        const func =
            /**
             *  生成した指定したパーサの処理を繰り返すパーサ
             * @param {string} target       ソースコード
             * @param {integer} position    読取り位置
             * @return {ParseInfo}          パースした結果
             */
            (target, position) => {/*repeat*/
                if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
                let count = 0;
                let result = [];                                // スタックを確保
                while (true) {
                    const parsed = parser(target, position);    //  とりあえず実行
                    if (parsed === undefined) {
                        continue;
                    }
                    if (parsed.success) {                       //  成功した場合は
                        result.push(parsed.result);             //      結果をスタックに保持
                        position = parsed.position;             //      読取り位置を更新
                        count++;
                    } else {                                    //  失敗した場合は
                        break;                                  //      ここで中断
                    }
                }
                // end of while
                return new ParseInfo((min <= count) ? true : false, result, position);      //  読取った結果を返す  ※常にTrueを返す
            };
        return func;
    }
    /**
     *  選択パーサを作成する
     * @param {function, Array of function} ...parsers    パーサの配列
     * @return {function}           生成した選択パーサ
     */
    choice(...parsers) {
        const methodName = 'choice';
        // パーサに正規表現やテキストが混ざっていたらパーサに変換する
        parsers = this.normalizeParses(parsers);
        if (parsers === null) { console.error(`${methodName}: parsers is undefined.`); }
        if (parsers === undefined) { console.error(`${methodName}: parsers is undefined.`); }
        // パーサが単体の場合はそのまま返す
        if (parsers.length === 1) return parsers[0];
        // パーサが複数の場合は、いづれかが成功した場合に成功を返すパーサを渡す
        const func =
            /**
             *  生成した選択パーサ
             * @param {string} target       ソースコード
             * @param {integer} position    読取り位置
             * @return {ParseInfo}          パースした結果
             */
            (target, position) => {/*choice*/
                if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
                for (var i = 0; i < parsers.length; i++) {
                    var parsed = parsers[i](target, position);
                    if (parsed === undefined) {
                        continue;
                    }
                    if (parsed.success) {                           // パース成功したら結果をそのまま帰す
                        return parsed;
                    }
                }
                return new ParseInfo(false, null, position);
            };
        return func;
    }
    /**
     *  パーサに正規表現やテキストが混ざっていたらパーサに変換する
     * @param {parser, Array of parser} parser
     */
    normalizeParses(parser) {
        //  配列の場合
        if (Array.isArray(parser)) {
            const parsers = parser;
            for (let i = 0; i < parsers.length; i++) {
                parsers[i] = this.normalizeParses(parsers[i]);
            }
        } else {
            //  配列以外の場合
            //  regexp
            if (parser instanceof RegExp) {
                parser = this.regexp(parser);
            }
            //  string
            if (typeof parser === "string" || parser instanceof String) {
                parser = this.token(parser);
            }
        }
        return parser;
    }
    /**
     *  連結パーサを作成する
     * @param {function, Array of function} parsers...    結合するパーサの配列
     * @return {function}           生成した連結パーサ
     */
    sequnce(...parsers) {
        const methodName = 'sequnce';
        // パーサに正規表現やテキストが混ざっていたらパーサに変換する
        parsers = this.normalizeParses(parsers);
        if (parsers === null) { console.error(`${methodName}: parsers is undefined.`); }
        if (parsers === undefined) { console.error(`${methodName}: parsers is undefined.`); }
        // パーサが単体の場合はそのまま返す
        if (parsers.length === 1) return parsers[0];
        // パーサが複数の場合は、全て成功した場合に成功を返すパーサを渡す
        const func =
            /**
             *  生成した連結パーサ
             * @param {string} target       ソースコード
             * @param {integer} position    読取り位置
             * @return {ParseInfo}          パースした結果
             */
            (target, position) => {/*sequnce*/
                if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
                const oldPosition = position;
                let result = [];
                for (let i = 0; i < parsers.length; i++) {
                    const parsed = parsers[i](target, position);
                    if (parsed === undefined) {
                        continue;
                    }
                    if (parsed.success) {
                        result.push(parsed.result);
                        position = parsed.position;
                    } else {
                        parsed.position = oldPosition;
                        return parsed;                              // 一つでも失敗を返せば、このパーサ自体が失敗を返す
                    }
                }
                return new ParseInfo(true, result, position);
            };
        return func;
    }
    /**
     *  オプションパーサを作成する
     * @param {function, Array of function} parser
     * @return {function} 生成したオプションパーサ
     */
    option(...parsers) {
        const methodName = 'option';
        // パーサに正規表現やテキストが混ざっていたらパーサに変換する
        parsers = this.normalizeParses(parsers);
        if (parsers === null) { console.error(`${methodName}: parsers is undefined.`); }
        if (parsers === undefined) { console.error(`${methodName}: parsers is undefined.`); }
        const parser = this.choice(...parsers);
        if (!parser instanceof Function) { console.error(`${methodName}: parser is not a function.`); }
        const func =
            /**
             *  生成した連結パーサ
             * @param {string} target       ソースコード
             * @param {integer} position    読取り位置
             * @return {ParseInfo}          パースした結果
             */
            (target, position) => {
                if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
                var result = parser(target, position);          //  とりあえず実行
                //  パースに成功したら、その結果を返す
                //  失敗したら、エラった訳では無いので、trueを返す
                return (result.success) ? result : new ParseInfo(true, null, position);
            };
        return func;
    }
    /**
     *  正規表現パーサを作成する
     * @param {RegExp} regexp_      正規表現
     * @return {function}           生成した正規表現パーサ
     */
    regexp(regexp_) {
        const methodName = 'regexp';
        if (regexp_ === null) { console.error(`${methodName}: regexp_ is null.`); }
        if (regexp_ === undefined) { console.error(`${methodName}: regexp_ is undefined.`); }
        if (!regexp_ instanceof RegExp) { console.error(`${methodName}: regexp_ is not a RegExp.`); }
        regexp_ = new RegExp(
            regexp_.source + '|(.*)',
            (regexp_.global ? 'g' : '') +
            (regexp_.ignoreCase ? 'i' : '') +
            (regexp_.multiline ? 'm' : '')
        );
        const func =
            /**
             *  生成した正規表現パーサ
             * @param {string} target       ソースコード
             * @param {integer} position    読取り位置
             * @return {ParseInfo}          パースした結果
             */
            (target, position) => {/*regexp*/
                if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
                regexp_.lastIndex = 0;
                var regexResult = regexp_.exec(target.slice(position));     //  とりあえず正規表現で実行
                const last = regexResult.pop();
                if (last === undefined) {                                   //  パターン最後の(.*)がundefinedならexecは成功
                    position += regexResult[0].length;                      //      読取り位置を更新し
                    return new ParseInfo(true, regexResult[0], position);   //      正規表現の結果を返す
                } else {
                    return new ParseInfo(false, null, position);            //  nullなので失敗
                }
            };
        return func;
    }
    /**
     *  遅延実行するパーサを作成する
     * @param {function} callback   遅延実行する処理
     * @return {function}           生成した遅延実行するパーサ
     */
    lazy(callback) {
        const methodName = 'lazy';
        if (callback === null) { console.error(`${methodName}: callback is null.`); }
        if (callback === undefined) { console.error(`${methodName}: callback is undefined.`); }
        if (!callback instanceof Function) { console.error(`${methodName}: callback is not a function.`); }
        var parse;
        const func =
            /**
             *  生成した遅延実行するパーサ遅延実行するパーサ
             * @param {string} target       ソースコード
             * @param {integer} position    読取り位置
             * @return {ParseInfo}          パースした結果
             */
            (target, position) => {/*lasy*/
                if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
                if (!parse) {
                    parse = callback();
                }
                return parse(target, position);
            };
        return func;
    }
    /**
     *  パーサの結果を加工するパーサを作成する
     * @param {function} parsers    sequnce用パーサ
     * @param {function}    fn      結果を加工する処理
     * @return {function}           生成した遅延実行するパーサ遅延実行するパーサ
     */
    map(...parsersAndFn) {
        const methodName = 'map';
        const fn = parsersAndFn.pop();    // 最後は結果を加工する処理
        const parsers = parsersAndFn;
        // fn定義漏れチェック
        if (fn === undefined) { console.error(`${this.className}.${methodName}: fn is undefined.`); return undefined; }
        if (!fn instanceof Function) { console.error(`${this.className}.${methodName}: fn not is Function.`); return undefined; }
        //  parserをsequnce化
        const parser = this.sequnce(...parsers);
        if (parser === null) { console.error(`${methodName}: parser is null.`); }
        if (parser === undefined) { console.error(`${methodName}: parser is undefined.`); }
        if (!parser instanceof Function) { console.error(`${methodName}: parser is not a function.`); }
        const func =
            /**
             *  生成した遅延実行するパーサ遅延実行するパーサ
             * @param {string} target       ソースコード
             * @param {integer} position    読取り位置
             * @return {ParseInfo}          パースした結果
             */
            (target, position) => {/*map*/
                if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
                const result = parser(target, position);
                if (result.success) {
                    const resultFn = fn(result.result);
                    return new ParseInfo(result.success, resultFn, result.position);
                } else {
                    return result;
                }
            };
        return func;
    }
}

今回の「車輪の再発明」

文法解析は再呼び出しダラケなので、正直コールスタックを読んでも訳が判らない。

const _debugPrint = (target, position) => {
    const text = this.positionText(target, position);
    console.log(`_trap: '${text}'`);
    return undefined;
};

これをパーサのパラメータの文法部分に

const syntax = _map(
    rule_list, _debugPrint, /\s*/
    , (parsed) => {
        return { _syntax: parsed[0] };
     });

のように差し込むと通過した時の文字の位置がコンソールに出るのでソコからあたりを付けられる。

※実績:

一度もword部まで到達してないっぽい?

どうやらコメントのトコの正規表現が怪しいなぁ?

そんな時には使えるデバッグプリント。



[javascript]BNF

とっても古い2016年の記事に構文解析と云えば、BNF か ABNF か EBNFかな?作れたのは字句解析まで・・・orzが放置してたので、やってみた。

wikipediaのBNF表記ではうまく動作しないので、

<expression> ::= <list> | <list> "|" <expression>
<list> ::= <term> | <term> <opt-whitespace> <list>

空白の判定を必須とオプションに分け、listの式も面倒な方を優先させ、

<opt-whitespace> ::= /\x20*/
<whitespace>     ::= /\x20+/
<expression>     ::= <list> <whitespace> "|" <whitespace> <expression> | <list>
<list>           ::= <term> <whitespace> <list> | <term>

と変更した結果、ソースはこんな感じになった。rule-nameの表現がクドいので変更した。

なお、BNFに空文や改行の無い行が含まれる場合はエラる。

    /**
     *  BNFルールと文法を作成
     * @param {string} bnf      BNFテキスト
     * @param {function} proc   mapのproc
     * @returns mapまたはbnfパターンの戻り値
     */
    parseBNF(bnf, proc) {
        const opt_whitespace = this.regexp(/\x20*/);
        const whitespace = this.regexp(/\x20+/);
        const regexp = this.map(this.regexp(/(\/(?!\\)[^/]*\/)/), (parsed) => { /* regexp */
            return parsed;
        });
        const dqstring = this.map(this.seq(this.token('"'), this.regexp(/(?!\\)[^"]*/), this.token('"')), (parsed) => { /* dqstring */
            return parsed.flat().join('');
        });
        const sqstring = this.map(this.seq(this.token(`'`), this.regexp(/(?!\\)[^']*/), this.token(`'`)), (parsed) => { /* sqstring */
            return parsed.flat().join('');
        });
        const literal = this.choice(dqstring, sqstring, regexp);
        const rule_name = this.map(this.seq(this.token('<'), this.regexp(/[A-Za-z0-9-_]+/), this.token('>')), (parsed) => {
            //  [0]:    <
            //  [1]:    rule - name
            //  [2]:    >
            return parsed[1];
        });
        const term = this.map(this.choice(rule_name, literal), (parsed) => { /* term */
            return parsed;                      // literal
        });
        const list = this.lazy(() => {
            return this.map(this.choice(this.seq(term, whitespace, list), term), (parsed) => { /* list */
                if (Array.isArray(parsed)) {        // term, whitespace, list
                    return parsed.flat();
                }
                return parsed;                      //  term
            })
        });
        const expression = this.lazy(() => {
            return this.map(this.choice(this.seq(list, whitespace, this.token('|'), whitespace, expression), list), (parsed) => { /* expression */
                if (Array.isArray(parsed)) {        //  list, whitespace, this.token('|'), whitespace, expression
                    return parsed.flat();
                }
                return [parsed];                      //  list  ruleの処理を簡素にするため配列で渡す
            })
        });
        const line_end = this.regexp(/[\r\n]+/);
        const rule = this.map(this.seq(opt_whitespace, rule_name, opt_whitespace, this.token('::='), opt_whitespace, expression, line_end), (parsed) => { /* rule */
            //  [0]:    opt_whitespace
            //  [1]:    rule_name
            //  [2]:    opt_whitespace
            //  [3]:    this.token('::=')
            //  [4]:    opt_whitespace
            //  [5]:    expression
            //  [6]:    line_end
            const rule_name = parsed[1];
            const expr = parsed[5];
            const expression = expr.filter((token) => token.replace(/\x20+/, '').length > 0);
            return { name: rule_name, expression: expression };
        });
        const syntax = this.lazy(() => {
            return this.map(this.many(this.choice(rule, this.seq(rule, syntax))), (parsed) => { /* syntax */
                const rules = {};
                if (parsed !== undefined && parsed !== null) {
                    while (0 < parsed.length) {
                        const p1 = parsed.shift();
                        rules[p1.name] = p1.expression;
                    }
                }
                return rules;
            })
        });
        this.fSpaceSkip = false;
        const result = syntax(bnf, 0);
        if (!result.success) {
            console.log('パース失敗');
        } else if (bnf.length !== result.position) {
            console.log(`${bnf.length}文字中の${1 + result.position}文字目でパース失敗`);
        }
        if (proc) {
            return proc(result);
        }
        return result.result;
    }
    testParseBNF() {
        const bnf = `<syntax>         ::= <rule> | <rule> <syntax>
                     <rule>           ::= <opt-whitespace> <rule-name> <opt-whitespace> "::="  <whitespace> <expression>  <line-end>
                     <rule-name>      ::= "<" /[A-Za-z0-9-_]+/ ">"
                     <line-end>       ::= /[\r\n]+/
                     <opt-whitespace> ::= /\x20*/
                     <whitespace>     ::= /\x20+/
                     <expression>     ::= <list> <whitespace> "|" <whitespace> <expression> | <list>
                     <list>           ::= <term> <whitespace> <list> | <term>
                     <term>           ::= <rule-name> | <literal>
                     <literal>        ::= '"' <text> '"' | "'" <text> "'"  | "\`" <text> "\`" | <regexp>
                     <regexp>         ::= /.+/\n`;
        this.parseBNF(bnf, (parsed) => {
            const json = JSON.stringify(parsed.result, null, 2);
            const text = json
                .replace(/\[[\r\n]+/sg, "[")
                .replace(/(?!]),[\r\n]+/sg, ",")
                .replace(/[\r\n]+\x20+\][\r\n]+/sg, "]\n")
                .replace(/[\r\n]+(\s+\],)/sg, "$1\n")
                .replace(/\x20+/g, " ")
                ;
            console.log('result: ' + text);
        });

ソースの色付けが怪しいけど字句解析や文法系は一般的なソースコードの書き方ではないから仕方が無いか?(笑

ともあれ、これを動かして

result: {
 "syntax": [ "rule", "|", "rule", "syntax" ],
 "rule": [ "opt-whitespace", "rule-name", "opt-whitespace", "\"::=\"", "whitespace", "expression", "line-end" ],
 "rule-name": [ "\"<\"", "/[A-Za-z0-9-_]+/", "\">\"" ],
 "line-end": [ "/[\r\n]+/" ],
 "opt-whitespace": [ "/ */" ],
 "whitespace": [ "/ +/" ],
 "expression": [ "list", "whitespace", "\"|\"", "whitespace", "expression", "|", "list" ],
 "list": [ "term", "whitespace", "list", "|", "term" ],
 "term": [ "rule-name", "|", "literal" ],
 "literal": [ "'\"'", "text", "'\"'", "|", "\"'\"", "text", "\"'\"", "|", "\"`\"", "text", "\"`\"", "|", "regexp" ],
 "regexp": [ "/.+/"]
}

と出たので一段落。※RegExpなreplaceで短くなるように編集済み

とにかく、デバッグが大変。

一通り作ってから組み合わせてみて、動かしてもドコまで進行しているのかスグに判らなり迷子になってしまうので、tokenやregexpにブレークポイントを置いて、どの辺まで進行したのか?あたりを付けてから、通る様に文法を組み直した。

正規表現部分は、自身の /(\/(?!\)[^/]*\/)/ が通らないが、ほぼ同義の/.+/で通ったので良しとした。

mapやseqなどはspread-sheetカスタムエレメント3 のソースのまま。その元ネタは「Java パーサコンビネータ 超入門」で公開されたソースです。ソースを読んでもイミフなら元ネタを見た方がいいです。

あとは、各ルールで結果をちゃんとしたトークンに整えれば完成しそう。

    /**
     *  BNFルールと文法を作成
     * @param {string} bnf      BNFテキスト
     * @param {function} proc   mapのproc
     * @returns mapまたはbnfパターンの戻り値
     */
    parseBNF(bnf, proc) {
        const opt_whitespace = this.regexp(/\x20*/);
        const whitespace = this.regexp(/\x20+/);
        const regexp = this.map(this.regexp(/(\/(?!\\)[^/]*\/)/), (parsed) => { /* regexp */
            return { regexp: parsed.replaceAll(/\x20/g, "\\x20") };
        });
        const dqstring = this.map(this.seq(this.token('"'), this.regexp(/(?!\\)[^"]*/), this.token('"')), (parsed) => { /* dqstring */
            return { dqstring: parsed[1] };
        });
        const sqstring = this.map(this.seq(this.token(`'`), this.regexp(/(?!\\)[^']*/), this.token(`'`)), (parsed) => { /* sqstring */
            return { sqstring: parsed[1] };
        });
        const bqstring = this.map(this.seq(this.token('`'), this.regexp(/(?!\\)[^`]*/), this.token('`')), (parsed) => { /* bqstring */
            return { bqstring: parsed[1] };
        });
        const literal = this.choice(dqstring, sqstring, bqstring, regexp);
        const rule_name = this.map(this.seq(this.token('<'), this.regexp(/[A-Za-z0-9-_]+/), this.token('>')), (parsed) => {
            //  [0]:    <
            //  [1]:    rule - name
            //  [2]:    >
            return { rule_name: parsed[1] };
        });
        const term = this.map(this.choice(rule_name, literal), (parsed) => { /* term */
            return parsed;
        });
        //
        const flatArray = (parsed, typeName) => {
            if (Array.isArray(parsed)) {
                const stack = parsed.map((p) => {
                    p = flatArray(p, typeName);
                    if (p.type === typeName) {
                        return p.value;
                    }
                    if (p[typeName] !== undefined) {
                        return p[typeName];
                    }
                    return p;
                });
                return stack.flat();
            }
            return parsed;
        }
        const list = this.lazy(() => {
            return this.map(this.choice(this.seq(term, whitespace, list), term), (parsed) => { /* list */
                if (Array.isArray(parsed)) {
                    parsed = this.removeWhiteSpace(parsed);
                    const parsed0 = flatArray(parsed, "seq");
                    return { seq: parsed0 }; // term, whitespace, list
                }
                return parsed; //  term
            })
        });
        const expression = this.lazy(() => {
            return this.map(this.choice(this.seq(list, whitespace, this.token('|'), whitespace, expression), list), (parsed) => { /* expression */
                if (Array.isArray(parsed)) {
                    parsed = this.removeWhiteSpace(parsed);
                    if (parsed[1] === '|') {
                        const parsed0 = flatArray(parsed[0], "choice");
                        const parsed2 = flatArray(parsed[2], "choice");
                        return { choice: [parsed0, parsed2] };
                    }
                    return parsed; //  list, whitespace, this.token('|'), whitespace, expression
                }
                return parsed; //  list
            })
        });
        const line_end = this.regexp(/[\r\n]+/);
        const rule = this.map(this.seq(opt_whitespace, rule_name, opt_whitespace, this.token('::='), opt_whitespace, expression, line_end), (parsed) => { /* rule */
            //  [0]:    opt_whitespace
            //  [1]:    rule_name
            //  [2]:    opt_whitespace
            //  [3]:    this.token('::=')
            //  [4]:    opt_whitespace
            //  [5]:    expression
            //  [6]:    line_end
            const rule_name = parsed[1];
            const expr = this.removeWhiteSpace(parsed[5]);
            return { rule: { name: rule_name, expression: expr } };
        });
        const syntax = this.lazy(() => {
            return this.map(this.many(this.choice(rule, this.seq(rule, syntax))), (parsed) => { /* syntax */
                const rules = {};
                if (parsed !== undefined && parsed !== null) {
                    while (0 < parsed.length) {
                        const p1 = parsed.shift();
                        rules[p1.rule.name.rule_name] = p1.rule.expression;
                    }
                }
                return rules;
            })
        });
        this.fSpaceSkip = false;
        const result = syntax(bnf, 0);
        if (!result.success) {
            console.log('パース失敗');
        } else if (bnf.length !== result.position) {
            console.log(`${bnf.length}文字中の${1 + result.position}文字目でパース失敗`);
        }
        if (proc) {
            return proc(result);
        }
        return result.result;
    }
    /**
     * テスト
     */
    testParseBNF() {
        const bnf = `   <syntax>         ::= <rule> | <rule> <syntax>
                        <rule>           ::= <opt-whitespace> <rule-name> <opt-whitespace> "::="  <whitespace> <expression>  <line-end>
                        <rule-name>      ::= "<" /[A-Za-z0-9-_]+/ ">"
                        <line-end>       ::= /[\r\n]+/
                        <opt-whitespace> ::= /\x20*/
                        <whitespace>     ::= /\x20+/
                        <expression>     ::= <list> <whitespace> "|" <whitespace> <expression> | <list>
                        <list>           ::= <term> <whitespace> <list> | <term>
                        <term>           ::= <rule-name> | <literal>
                        <literal>        ::= '"' <text> '"' | "'" <text> "'"  | "\`" <text> "\`" | <regexp>
                        <regexp>         ::= /.+/\n`;
        this.parseBNF(bnf, (parsed) => {
            const json = JSON.stringify(parsed.result, null, 2);
            const text = json
                ;
            console.log('result: ' + text);
        });

の結果がちょっと長いので最深部の{}は手作業で短縮形に編集したものがコレ。

result: {
    "syntax": {
        "choice": [
            { "rule_name": "rule" },
            {
                "seq": [
                    { "rule_name": "rule" },
                    { "rule_name": "syntax" }
                ]
            }
        ]
    },
    "rule": {
        "seq": [
            { "rule_name": "opt-whitespace" },
            { "rule_name": "rule-name" },
            { "rule_name": "opt-whitespace" },
            { "dqstring": "::=" },
            { "rule_name": "whitespace" },
            { "rule_name": "expression" },
            { "rule_name": "line-end" }
        ]
    },
    "rule-name": {
        "seq": [
            { "dqstring": "<" },
            { "regexp": "/[A-Za-z0-9-_]+/" },
            { "dqstring": ">" }
        ]
    },
    "line-end": { "regexp": "/[\r\n]+/" },
    "opt-whitespace": { "regexp": "/\\x20*/" },
    "whitespace": { "regexp": "/\\x20+/" },
    "expression": {
        "choice": [
            {
                "seq": [
                    { "rule_name": "list" },
                    { "rule_name": "whitespace" },
                    { "dqstring": "|" },
                    { "rule_name": "whitespace" },
                    { "rule_name": "expression" }
                ]
            },
            { "rule_name": "list" }
        ]
    },
    "list": {
        "choice": [
            {
                "seq": [
                    { "rule_name": "term" },
                    { "rule_name": "whitespace" },
                    { "rule_name": "list" }
                ]
            },
            { "rule_name": "term" }
        ]
    },
    "term": {
        "choice": [
            { "rule_name": "rule-name" },
            { "rule_name": "literal" }
        ]
    },
    "literal": {
        "choice": [
            {
                "seq": [
                    { "sqstring": "\"" },
                    { "rule_name": "text" },
                    { "sqstring": "\"" }
                ]
            },
            {
                "choice": [
                    {
                        "seq": [
                            { "dqstring": "'" },
                            { "rule_name": "text" },
                            { "dqstring": "'" }
                        ]
                    },
                    {
                        "choice": [
                            {
                                "seq": [
                                    { "dqstring": "\`" },
                                    { "rule_name": "text" },
                                    { "dqstring": "\`" }
                                ]
                            },
                            { "rule_name": "regexp" }
                        ]
                    }
                ]
            }
        ]
    },
    "regexp": { "regexp": "/.+/" }
}

大体予想通りの結果が得られたのでBNFについてはコレで終了。

※空白を”\x20″と表現させようとしたが、文字列中にバックスラッシュ(\)があると、テキスト化した時に(\\)に変換してるっぽい。



[javascript]async await 連鎖

  • ドコかで同期を取るために awaitする
    • awaitを使用するためにその関数にasyncを付ける。
      • その関数を呼び出す関数も概ね後処理があるのでawaitする
        • awaitを使用するためにその関数にasyncを付ける。
          • …延々と連鎖

セルに表示するデータをindexedDBで取得するように修正したら・・・↑の様なことになった。

サクラエディタに関数とソレを参照する関数を階層リストを書いて漏れチェック。

幸いにもコノ程度で済んだ。

  • spreadSheet:getCellData
    • layout:createThTdElement
      • layout:createTrElement
        • layout:createTableElement
      • layout:createColThTdElement
        • layout:insertOverCol
          • command:moveCell
            • event:clickMouseEvent
            • layout:makePageContents
              • spreadSheet:makePageContents
    • layout:getCellData
      • command:openEntry
      • layout:isCellData
        • command:chkDirectionData
  • spreadSheet:setCellData
    • layout:setCellData
      • command.closeEntry
        • event:dblclickMouseEvent

そこまでは良かったが動作が変。

地道にチェックしていくと、Arrayのsomeのコールバックがasyncな場合でも同期を取らず、どんどん処理を進めていくコトが判明。

対処方法としては new PromiseでPromiseの配列を作ってPromise.allで完了待ちが筋だが、

const objRow = createHtmlElement('tr', attrs);
let w = 0;
if (nRow === 0) {//初期表示時   ※TODO 初期表示時は左上セルがA1と仮定
    const cols = this.spreadSheet.cols;
    for (let nCol = 0; nCol <= cols; nCol++) {
        const cell = await this.createThTdElement(objRow, nRow, nCol);
        w += cell.clientWidth;
        if (rectParent.width < w) {
            break;
        }
    }
}

外の変数を使っている場合が多々あったのでsome化したものをfor文に戻した。

rangeをPromise対応してみようか・・・

/**
 *  連番配列っぽいイテレータっぽいものを作成する
 * @param {integer} start           開始番号
 * @param {integer} end             終了番号
 * @returns {iterator}              開始番号から終了番号を格納した配列っぽいイテレータ
 */
const range = (start, end) => {
    let step = 1

    const rangeIterator = {
        some: function (func) {//someっぽく動作させる
            for (let cnt = start; cnt <= end; cnt += step) {
                let rc = func(cnt);
                if (rc) {
                    return true;
                }
            }
            return false;
        },
        map: function (func) {//mapっぽく動作させる
            const f = false;
            let aRc;
            if (f) {
                aRc = new Array(end - start + 1);
            }
            let index = 0;
            for (let cnt = start; cnt <= end; cnt += step) {
                const rc = func(cnt);
                if (aRc) aRc[index++] = rc;
            }
            return aRc;
        },
    };
    return rangeIterator;
}

無理っぽいなぁ~

/**
 *  連番配列っぽいイテレータっぽいものを作成する
 * @param {integer} start           開始番号
 * @param {integer} end             終了番号
 * @param {integer} step            ステップ
 * @returns {iterator}              開始番号から終了番号を格納した配列っぽいイテレータ
 */
const promiseRange = (start, end, step = 1) => {
    return rangeIterator = {
        some: function (func) {//someっぽく動作させる
            return new Promise(async (resolve, reject) => {
                for (let cnt = start; cnt <= end; cnt += step) {
                    let rc = await func(cnt);
                    if (rc) {
                        resolve(true);
                        return;
                    }
                }
                resolve(false);
            });
        },
        map: function (func) {//mapっぽく動作させる
            return new Promise(async (resolve, reject) => {
                let aRc = [];
                for (let cnt = start; cnt <= end; cnt += step) {
                    const rc = await func(cnt);
                    aRc.push(rc);
                }
                resolve(aRc);
            });
        },
    };
}

こんな風に使うんだけど

const cols = this.spreadSheet.cols;
await promiseRange(0, cols).some(async (nCol) => {
    const cell = await this.createThTdElement(objRow, nRow, nCol);
    w += cell.clientWidth;
    if (rectParent.width < w) {
        return true;
    }
    return false;
});

動くから良し。

ps.

だがしかし、後でasync付きのメソッドを呼び出す時にawaitし忘れガチ。なので、メソッド名のツールチップを見て戻り値がPromiseになってないか確認しないと同期ズレで酷い目に遭う。asyncは明示しないとスクリプト側が大変だと思うけど、awaitは暗黙の了解ぽく自動的に処理して欲しいなぁ。代わりに noWait実装でヨロ。

ps.2024/3/18

計算式にセル参照を追加したら、またasync/awaitの連鎖。しかし全般的にパーサやコールバックをasyncし、呼び出し側でawait するダケで済む。かと思ったが、Array.reduceでPromiseを回すと、パーサーコンピネータなので、

Array.reduce
  return new Promise((resolve,reject)=>{
 promise,then(()=>{
   ・・・
  Array.reduce
    return new Promise((resolve,reject)=>{
     promise,then(()=>{
     ・・・
    Array.reduce
       return new Promise((resolve,reject)=>{
        promise,then(()=>{
       ・・・
         resolve(xxx);

と各所(chiceやseq等で)階層ループになってしまう。突入するのはいいが、離脱する際に、どのArray.reduceに戻るか不安定。function () {…}.bind(this)のbindのthisとPromise.resolve(…)は仕様的にも相性が最悪だけど、reduceか階層ループのいづれかが無ければ、どうと云うことは無いので、Array.reduce(…)をfor(){…}に戻した。

その後、セル参照のある数式を連鎖して再計算させてみたら、indexedDBのトランザクションの階層的な利用は不可だったので、階層化しそうな処理をsetTimeoutでばらまいたが、トランザクションが終わらないうちに動き出すので、一旦配列に入れトランザクション完了後に、配列をpopで読出しつつsetTimeoutでばらまいた。そうするしかない。順序を守りつつ非同期で処理しないとトランザクションが階層化してしまうからね。



「javascript]謎な不安定さ

contenteditableのカスタムエレメント版 の修正も大方終わった。

気がするけど、見る度に手直ししている。(大笑

‘”.*?”‘では、リテラル中のバッククォートを処理できない。なので、JDOCやコメント部はリテラルとと一緒に処理し、その他は字句解析し直すことにした。※convertJavascriptToHTML1~2

javascriptでは、'”(\\\\.|.)*??”‘がエラるが、'”(\\\\.|.)*?”‘なら通るので、matchの戻り値[0]のテキストを使い括弧で括ってないパターンの部分も取れる。※convertJavascriptToHTML3

多分これで大丈夫。(でも心配だから、1~2も残しておく。

JDOCも変になっていた。最後の字句解析で行ごとにspanで括る様にしたので再帰したりすると変更漏れ

ちょっと修正すると動作がガタガタになりやっと安定すると行番号の幅が変に狭いので見てみるとVisualStudioCode??が謎に}を追加しててcssが効かなくなってたりするのは比較的解りやすいけど…

`.code>.row2 {
    margin-left: 1.5rem;
} ☚❓
}\n`,
`.code>.row3 {
    margin-left: 2rem;
} ☚❓
}\n`,

そのガタガタ動作を追っていった最中に気が付いたコトをメモっておく。

staticな初期設定あるいはthisの話

class Sample {
  styles={};
  constructor(){
    this.styles['one'] = 1;
    this.styles['two'] = 2;
    this.styles['three'] = 3;
  }
}

は上手く動作するけど、staticにすると

class Sample {
  static _ = Sample.setup();
  static styles={};
  Sample.setup(){
    Sample.styles['one'] = 1;
    Sample.styles['two'] = 2;
    Sample.styles['three'] = 3;
  }
}

と初期設定をした後に

static styles={};

が動き出す場合があるので、

class Sample {
  static _ = Sample.setup();
  static styles;
  Sample.setup(){
    if(!Sample.styles) { Sample.styles={}; }
    Sample.styles['one'] = 1;
    Sample.styles['two'] = 2;
    Sample.styles['three'] = 3;
  }
}

の様に各static要素の初期設定順に依存しないようにした方がいい。

また、

class Sample {
  static _ = Sample.setup();
  static setup() {
       Sample.field1 = 10;
  }
  static field1;

とクラス名のトコにthisに変えてもstatic同士なら支障は無い。

class Sample {
  static _ = this.setup();
  static setup() {
       this.field1 = 10;
  }
  static field1;

がインスタンスを得た時点でthis経由でstaticなものは見えないので動きが変になりやすい。

更にサブクラスからSample.setup()の様に呼び出されると

class Sample {
  static _ = this.setup();
  static setup() {
       this.field1 = 10;
  }
  static field1;

class SubSample extends Sample{
  static _ = Sample.setup();

サブクラス側にstaticなfield1が出来てしまう様だ。

コレクション型フィールドのgetterの戻り値を使いまわす場合

一番悩ましいのはコレ。

class Sample {
  styles={};
  constructor(){
    this.styles['one'] = 1;
    this.styles['two'] = 2;
    this.styles['three'] = 3;
  }
  getStyles() { return this.styles; }
}

・・・

class SubSample extends Sample {
    ・・・
    const styles = this.getStyles();
    {
    styles['four'] = 4;
      {
      styles['five'] = 5;
        {
          styles['six'] = 6;
             ・・・
               {
               styles['ten'] = 10; 
  ※もうこのあたりまでくるとthis.getStyles();との距離がかなり離れてしまい
  this.stylesに更新した内容(=4のあたりから=10まで)が反映されない時がある。

単にソースが長くなってくると起きることがあるので、

class Sample {
  styles={};
  constructor(){
    this.styles['one'] = 1;
    this.styles['two'] = 2;
    this.styles['three'] = 3;
  }
  getStyles() { return this.styles; }
}
・・・
class SubSample extends Sample {
・・・
    const list = {
    'four': 4,
    'five': 5,
    'six': 6,
・・・
    'hundred': 100,
    };

な感じで

    const styles = this.getStyles();
    Object.keys(list).forEach((key) => styles[key] = list[key]);

とできるダケ「this.getStyles()」を近づけて逃げるよりも

    Object.keys(list).forEach((key) => this.styles[key] = list[key]);

this.stylesに直接入れるか、処理の戻り値でthis.stylesを上書きしてしまった方が良い。

class Sample {
  styles={};
  constructor(){
    this.styles['one'] = 1;
    this.styles['two'] = 2;
    this.styles['three'] = 3;
  }
  setStyles(v) { this.styles = v; }
  getStyles() { return this.styles; }
  ・・・・
  method() {
    ・・・・
    const styles = this.getStyles();
    ・・・・
  this.setStyles(styles);
  }
}


[javascript]クラスの名前を調べる

ログウインドウの右端にファイル名が付くけど、クラス名も書き出したい。

class xxxx{
  static className = this.name;
  ...
}

は安定してクラス名が取得できるが、これは、staticなメソッドのみ参照可能で、インスタンス依存のメソッド用に

class xxxx{
  className;
  constructor() {
    this.className = this.name;
    ...
  }
  ...
}

class xxxx{
  className;
  constructor() {
    this.className = this.constructor.name;
    ...
  }
  ...
}

のいづれかが必要だった。どっちが常に正しいという訳では無いので、

static className = this.name;
className;
constructor() {
  this.className = this.name ?? this.constructor.name;
  ...
}

~なぁあたりが良さそうだ。

??はあまり使わないけど、Null 合体演算子で、

「とりあえず【まともな値】を返す」場合に便利な演算子。

新しいパターンが増えたら

static className = this.name;
className;
constructor() {
  this.className = this.name ?? this.constructor.name ?? {新しいパターン};
  ...
}

にすればいいだろう。

これで各メソッドに

    メソッド名 (...) {
        const methodName = '{メソッド名}';
        console.debug(`${this.className}.${methodName}(...)`);

と書ける。

例外処理を発生してスタックを調べる事もできたけど、イマイチ感がある。

const getCurrentLineInfo = () => {
    try {
        throw new Error('getCurrentLineInfo Error');
    } catch (ex) {
        let stack = ex.stack.split('\n');
        let target = stack[2];
        // パターン1:at xxClass.yyMethod (https://{domain}/{path}/{filename}:{line}:{column})
        // パターン2:at xxClass.yyMethod (file:///{drive}/{path}/{filename}:{line}:{column})
        let match = target.match(/\s*at\s*([_a-zA-Z]+)[.]([_a-zA-Z]+)\s*([(])(.+?)([)])/);
        // /\s*at\s*([_a-zA-Z]+)[.]([<>_a-zA-Z]+)\s*(?<=[(])(.+?)(?=[)])/ にすると失敗?
        let className = match[1];
        let methodName = match[2];
        let url = match[4];
        url = url.split(':');
        let type = url.shift();
        switch (type) {
            case 'http':
            case 'https':
                var domain_fullpath = url.shift();
                var [_, _, domain, ...fullPathName] = domain_fullpath.split('/');
                fullPathName = '/' + fullPathName.join('/');
                var row = url.shift();
                var column = url.shift();
                break;
            case 'file':
                var drive = url.shift();
                var fullPathName = url.shift();
                var row = url.shift();
                var column = url.shift();
                break;
        }
        let fileName = fullPathName.split('/').slice(-1)[0];
        let fullPath = fullPathName.substring(0, fullPathName.length - fileName.length - 1);
        var rc = {
            type: type,
        }
        if (type == 'file') {
            Object.assign(rc, {
                drive: drive,
            });
        } else {
            Object.assign(rc, {
                domain: domain,
            });
        }
        Object.assign(rc, {
            fullPath: fullPath,
            fileName: fileName,
            className: className,
            methodName: methodName,
            row: row,
            column: column,
        });
    }
    return rc;
}
  • 名前を調べる目的の割に長すぎる
  • 関数を呼ぶ度に例外処理を起こすのは実行時間に響きそう
  • chromeのスタック情報に依存したコードなので他ではエラりそう

サンプルでボタンを押すと表示する。

getCurrentLineInfo.jsのSampleClassクラスのexec()を呼び出した場合、
{
 "type": "https",
 "domain": "ssiscirine.iobb.net",
 "fullPath": "/sample/stack",
 "fileName": "getCurrentLineInfo.js",
 "className": "SampleClass",
 "methodName": "exec",
 "row": "5",
 "column": "12"
}
getCurrentLineInfo.jsのgetCurrentLineInfo()を直接呼び出した場合、
{
 "type": "https",
 "domain": "ssiscirine.iobb.net",
 "fullPath": "/sample/stack",
 "fileName": "stack.html",
 "className": "HTMLButtonElement",
 "methodName": "<anonymous>",
 "row": "14",
 "column": "43"
}

FireFoxではクラス名は無いけどメソッド名(関数名?)まで取れるのでサンプルの方は対応している。

HTMLのscriptやonclickからgetCurrentLineInfo()を直接呼び出した場合は行列位置は正しいので使えるけど、他は当てできないね。

それにしても

out.innerText = JSON.stringify(getCurrentLineInfo(),null," ");  ※" "は全角1文字

と、インデントを指定すると、改行もしてくれるので読みやすい。※”\t”も良いらしい。



[javascript]spread-sheetカスタムエレメント

spread-sheetカスタムエレメントってありそうで無いっぽいので作ってみた。

セルに文字入力できるダケのしろもの。

  • IMEがONになると動きがおかしい
  • 範囲指定ができない
  • 計算できない
    • 簡単な計算は可、セルアドレスでセル値参照も可
    • 関数が何も無い
      • sum,count,average,min,max
    • 値の変動に計算式を連動
      • 範囲指定時の連動が未実装
  • 入力したデータの保存ができない
    • indexedDBにセル値を保持
      • indexedDBのスコープがドメイン単位なので別ページと被ってしまいがち
        • databasse属性にurlから置換する%domain%,%path%,%filename%を追加
          • 例:databasename=”%domain%%path%%filename%_スプレッドシート1”
            • 結果はDevToolsのアプリケーションのindexedDBを参照
  • セル幅や高さが変えられない
  • 文字のスタイルを変えられない
  • javascriptからシートのデータを参照できない
  • ステータスバーが無い
  • 数式入力バーが無い
  • セルを移動し画面外に出ても画面がスクロールしない
    • 行列タイトルがスクロールロックしてない
      • スクロールバーが無反応
  • マクロが無い
  • データのインポート、エクスポートが無い
  • グラフ、絵、矢印とか貼れない
  • セルに名前が付けられない
  • シートが1枚ダケ
  • 列・行の追加や削除ができない
  • カットアンドペーストができない

等々いっぱい未実装。

1つのクラスでは長すぎるので分割してみた。

  • SpreadSheetCustomElement   全体(this)
    • this.customElement     カスタムエレメント登録Callback等
    • this.event         イベント処理
    • this.command       コマンドっぽい処理
    • this.layout         レイアウト処理

各処理からはthis.spreadsheet経由で別処理を呼び出す必要がある。

function OpenEntry () {
  let rect = this.spreadsheet.layout.cellEditing.getBoundingClientRect(); //編集するセルの位置を求め
  this.spreadsheet.layout.divTextarea.top = rect.top; //テキストエリアを内包するdivをソコに移動させ
  this.spreadsheet.layout.divTextarea.left = rect.left;
  this.spreadsheet.layout.textarea.value = event.key; //テキストエリアに入力した文字を入れ
  this.spreadsheet.layout.divTextarea.style.display = 'block'; //テキストエリアを内包するdivを表示
}

this.xxxx.yyyy.zzzzと長々と詠唱するのは読みにくいので

function OpenEntry () {
  const spreadsheet = this.spreadsheet;
  const layout = spreadsheet.layout;
  let rect = layout.cellEditing.getBoundingClientRect(); //編集するセルの位置を求め
  layout.divTextarea.top = rect.top; //テキストエリアを内包するdivをソコに移動させ
  layout.divTextarea.left = rect.left;
  layout.textarea.value = event.key; //テキストエリアに入力した文字を入れ
  layout.divTextarea.style.display = 'block'; //テキストエリアを内包するdivを表示
}

constを使い短縮化した。

後で割り当てを変更する時には直しやすいかもしれない。(気がする。

this.spreadsheet.layout.divTextarea.top 
// divTextareaをlayoutからshadowへ移動
=>this.spreadsheet.layout.spreadsheet.shadow.divTextarea.top 
// divTextareaをshadowからlayoutへ戻す
=>this.spreadsheet.layout.spreadsheet.shadow.spreadsheet.layout.divTextarea.top 
// は?

となるよりは

layout.divTextarea.top 
// divTextareaをlayoutからshadowへ移動
=>shadow.divTextarea.top 
// divTextareaをshadowからlayoutへ戻す
=>layout.divTextarea.top 
// 良し!

の方がマシ。

さて!いざ、やってみるとVScodeのリファクタリングが不十分でサクラエディタで(ガシガシ

になるのはいつものこと。

カスタムエレメントの宣言をクラスの出す事は可能だけど、クラスのstatic getterがconstructorに作られるので差し替え不可、無名のクラス文でラップするとコードがとても長くなるので取止め。

ps.

行列タイトルロックできたけど、タイトルロックの状態の表と元のサイズの表の差をスクロール量としてスクロールバーのスライダー位置を調整しているので、まだスライダーを触ってもタイトルロックの状態は変動しない。

ps

spread-sheetカスタムエレメント3 セルの範囲をMS-Office365のExcelまで拡大してみたところ、Ctrl+↓↑で移動すると1,048,576行もあるせいで移動完了まで「しばらくお待ちください」状態。カーソルをあちこちと移動するとテーブルが右端から壊れていく。

ps.

多少良くなったので、spread-sheetカスタムエレメント3を更新、データもindexedDBに保持したので、ブラウザが重くなるかもしれない。また時々DB構成を変えるかもしれないので、devToolsのアプリケーションでストレージからIndexDBに▶が付いていたらDB削除してから見た方が良いかも。まだカーソルジャンプ中に帰ってこない時がある。

ps.

indexedDB周りをdatabase.jsに分離。データベースも’R0000001’とか’C00002’など固定文字列も止めてinteger化したが今までどおりVersionアップ処理はしていない。

ps.

A1セルから右端まで移動し一番下の行へジャンプするとセルが隠れた。同様に一番下の行のA列から右端の列までジャンプし一番上の行へジャンプすると右側の隙間がひろがっていた。この修正中に列を追加する処理を呼び出す側がawaitしていなかったせいで画面が壊れることがあった。

カスタムエレメントの属性にdatabasenameを追加し、スプレッドシート毎にindexedDBのデータベースを分離可能に。

ページ移動(PageUp/Down)で同期を取っていたら、キーリピートで並行に処理が走ってしまい画面構成がクズれるので、全般的にキー操作はsetTimeoutで後回しにすることで並行に処理が走らない様にした。

ps.2024/3/18

indexDBの構成を変更!パーサーコンビーネータを組み込んで数値やセルアドレスで四則演算程度の数式を計算できた。但し、indexedDBが非同期なので、パーサーコンビーネータでArray.reduceを使った箇所の同期ズレ(Promiseレーシングや無限ループ)を対処できずfor文に戻した。

セル値の変動に連動して再計算させてみたが、inndedDBのトランザクションは階層化できない様で、トランザクションを流しながらセルデータをinndedDBで読み書きした後に、カーソルを移動させると、トランザクションが終了な済みエラーになる。また、データベースに複数キーのインデックスがあると、単キーのインデックスでもカーソルの範囲(上下限)は配列で指定しなければいけない。bound([1],[1])でデータが取れるけど、bound(1,1)ではスカっと終了する。

そう云えば、数式とかデータに空白が混入するとパーサのパターンにマッチしない。パーサの処理の先頭に空白の読飛ばし処理を追加した。でも数式は先頭文字が=の場合だけに限定したいので、読飛ばしをON・OFFするようにした。seqとchiceにパラメータに混入させたのでソコはチョットごまかし。

#初期設定
this.fSpaceSkip = false;
・・・
#数式の構文のルート木
const exprCell = this.map(this.seq(this.token('='), () => { this.fSpaceSkip = true }, expr), async (parsed) => {
・・・
#値の構文のルート木
const cell = this.choice(exprCell, () => { this.fSpaceSkip = false }, valueCell);
・・・
#各パーサの処理
async (target, position) => {
  if (this.fSpaceSkip) { [target, position] = this.skipSpace(target, position); }
・・・
}
#seqまたはchoiceのforループ
for (var i = 0; i < parsers.length; i++) {
  var parsed = await parsers[i](target, position);
  if (parsed === undefined) {
    continue;
  }
  ・・・
}

TextAreaを表示中に他のセルをクリックするとTextAreaを閉じそのセルをフォーカスっぽい表示にするけど、その際にクリックイベントで渡されたevent.targetの中身が書換わってスクリプトが暴走状態になってたので即保持しておく。※Promiseを使い出してからはよく起きる現象

ps.2024/3/19

sum関数がちょっと動き出した。なのでindexedDBの構成がまた変わった。min.max,averageもセルの範囲指定でも計算可。※結果が正しいかもしれない。

ps.2024/3/20

indexedDBのセルデータをJSON風に変更で文字列型が修正漏れ。sum関数等でほぼ同じコードが続くのでコールバック式に変更。Deleteキーでセルデータを空白に。=A1などアドレスや範囲が結果になる場合にそれっぽく表示。1+2は計算するけど1+2+3は面倒そうなので後回しにしたら、関数のパラメータも3個あるとダメだったし、アドレスに小文字を使うと変で、=A1:C7と範囲値の場合も中途半端で、セルを空欄にするとundefined(ある意味正しいけど)になってたので、見直し。確認のためにindexedDBを見ると、画面からデータを削除するとレコードの値が空になってたダケだったのでレコードごと消える様に修正。indexedDBのデータが同じドメインなら共有だがdatabasename属性に置換文字列を追加し名前で切り分け可能にした。

ps.2024/3/21

セルデータをJSON風化のmin.max関数の修正漏れ。(v)?v:”でnullやundefinedが表示されない様にしたら0も表示しなくなっていた。date型で日付と時刻の入力でDate型のformatを実装、boolean型の入力、now(), today()を実装。論理式を実装しないとif関数が作れない。round()とvlookup()はコードしてみたダケ。

ps.2024/3/24

a1形式のaddress1をaddressに。関数をグループ管理に変更中。後、細かいバグ。セル上にマウスを移動するとデバッグ用ツールチップを表示。

ps.2024/3/25

データ型をstringをtextに変更。F2で入力すると直前のセルで入力した内容がチラっと見えていた。

パーサーコンビネータに論理式等を追加したのでexpr(計算式の根っこ)の呼び出しが激増しasync、 awaitも多く連動計算の待ち時間が長くなってきてsetTimeoutで連動計算を呼び出していることもああり、僅かなミスでスタックオーバーフローやメモリ不足に陥りやすくなっている。

ps.2024/3/26

計算式のパース(parse)と計算(calc)を分離、indexedDBの式はパース結果(トークン風)。これでA1とR1C1の切り替えができそう。少し寄り道してmapメソッドをBNFテキストからパーサーコンビネータの文法部分を作るようにしてしまいたい。今は this.map(this.seq( のようにthis と括弧が多いから。

ps.2024/3/27

演算子*と/が+ーな計算をしていた。関数をexprメンバーに文字で格納していた名残で式のトークンを配列で格納していたがname,parametersに分けた。式を再入力するとexprがname,parametersに変わる様に暫くは式の文字列化と計算でexprも併用させる。アドレス型のaddressを配列に変更したのでそのうちアドレス範囲型を無くす予定。




top