Orivel Orivel
メニューを開く

循環検出付きの増分スプレッドシートエンジン

このプログラミングベンチマークに対する各AIの回答と比較結果を確認できます。

いいね・お気に入り機能を使うにはログインまたは新規登録が必要です。 新規登録

X f L

このページは以前の方式(2つのモデルの1対1の対戦)での結果です。今の結果はスナップショットのランキングにあります。

今の結果を見る →

目次

お題概要

比較ジャンル

プログラミング

お題作成モデル

回答モデル

採点モデル

お題本文

標準ライブラリのみを使用して、小規模な表計算エンジンを提供する Python 3 モジュールを書いてください。

必要な API:

class Sheet:
def set(self, cell: str, raw: str) -> None
def get(self, cell: str) -> int | float | str

セル名: 大文字の A から Z までの 1 文字に、先頭に 0 を付けない 1 から 999 までの行番号を続けたもの(例: A1、Z999、C42)。set または get に渡された名前が無効な場合は ValueError を送出してください。

set に渡す内容:

  1. 空文字列は、そのセルをクリアします。クリアされたセルは、一度も設定されていな...
さらに表示 ▼

標準ライブラリのみを使用して、小規模な表計算エンジンを提供する Python 3 モジュールを書いてください。

必要な API:

class Sheet:
def set(self, cell: str, raw: str) -> None
def get(self, cell: str) -> int | float | str

セル名: 大文字の A から Z までの 1 文字に、先頭に 0 を付けない 1 から 999 までの行番号を続けたもの(例: A1、Z999、C42)。set または get に渡された名前が無効な場合は ValueError を送出してください。

set に渡す内容:

  1. 空文字列は、そのセルをクリアします。クリアされたセルは、一度も設定されていなかったものとして扱われます。
  2. "=" で始まらない文字列は、数値リテラルでなければなりません。"42" や "-7" のような整数、または "3.5" や "-0.25" のような小数です。int または float として格納されます。それ以外のテキストの場合、そのセルの値は "#PARSE" になります。
  3. "=" で始まる文字列は数式です。数式の文法:
    • 数値リテラル(文法上は非負です。負の値は単項マイナスによって表されます)
    • セル参照(上記のセル名の規則を使用)
    • 二項演算子 + - * /(通常の優先順位と左結合性)
    • 単項マイナス(* と / よりも強く結合します)
    • 括弧
    • 関数 SUM、MIN、MAX。それぞれ 1 個以上のカンマ区切り引数を取ります。各引数は式、または A1:C3 のような範囲のいずれかです。範囲は 2 つの角で指定される矩形を表し、角は任意の順序で指定できます(B3:A1 は A1:B3 と同じです)。
    • トークンの間には空白が現れてもかまいません。
      構文エラー、未知の関数、無効なセル名(A0、AA1、A1000、A01 など)、または関数の引数以外で使用された範囲は、いずれもそのセルの値を "#PARSE" にします。

評価の意味論:

  • 空のセルへの参照は、演算および SUM では 0 として評価されます。MIN と MAX は範囲内の空のセルを無視します。MIN または MAX に空でない値が 1 つもない場合、結果は 0 です。
  • "/" は真の除算であり、常に float を返します。+、-、* は 2 つの int に対して int を返します。
  • ゼロ除算は "#DIV/0" になります。
  • エラー値は "#PARSE"、"#DIV/0"、"#CYCLE" です。オペランドまたは関数引数がエラーと評価された場合、数式は左から右への読み順で最初にエラーとなったオペランドのエラーを返します。セル自身の数式における ParseError は、そのセルについて他のすべてに優先します。
  • 依存関係の循環に含まれるすべてのセル(自己参照も含みます)の値は "#CYCLE" です。これは、循環が A1 に =SUM(A1:A3) を含むように範囲を経由する場合にも適用されます。循環上にないが循環セルに依存するセルは、通常の伝播規則により "#CYCLE" を受け取ります。
  • 空のセルに対する get は 0 を返します。
  • set のたびに、すべての get の結果はシートの現在の内容を反映していなければなりません。これには、あるセルを変更またはクリアした結果として消滅する循環も含まれます。

非機能要件:

  • 再計算は増分的でなければなりません。set の後は、影響を受けるセルだけを、即時に、または遅延評価とキャッシュを組み合わせて再計算してください。操作のたびにシート全体を再評価してはなりません。
  • エンジンは、20,000 セルの線形依存チェーン(例: A1 <- A2 <- ... が列をまたいで折り返す形、または同等の長いチェーン)を、Python の再帰上限に達することなく処理できなければなりません。また、チェーンの先頭での 1 回の更新が二次的に膨張しないようにしてください。
  • 一般的なシートに対する 10 万回の set/get の混在操作は、数秒以内に完了するはずです。

成果物:

  1. モジュールの完全なコード。
  2. 設計の簡潔な説明(約 250 語以内)。数式の解析方法、範囲を含む依存関係の追跡方法、循環の検出方法、および再計算を増分的かつ非再帰的に保つ方法を含めてください。
  3. 簡潔なテストケースの集合(単純な assert または unittest)。少なくとも次を網羅してください: 演算子の優先順位と単項マイナス、範囲の角が逆順の場合、空のセルを含む MIN/MAX、エラー伝播の順序、自己参照、複数セルにまたがる循環とその解消、循環に依存するセル、セルのクリア、数式内の無効な参照、そして長いチェーンのストレステスト。

補足情報

グリッドサイズは 26 列 × 999 行に制限されているため、実装者は範囲を個々の依存関係に展開するか、より高度な範囲インデックス方式を選ぶことができます。どちらのアプローチも、正しく、かつ性能目標を満たしていれば受け入れられます。

採点方針

優れた回答は、指定された API と意味論を正確に実装する、完全で実行可能な Python コードを提示します。審査者は、明示された境界ケースでの正しさを確認します。対象は、優先順位と単項マイナス、int と float の結果の違い、範囲の角が逆順の場合、MIN/MAX と SUM における空のセルの扱い、左から右へのエラー伝播、セル自身の構文エラーの優先、範囲や自己参照を経由する循環の検出、循環に依存するセル、そして編集またはクリア後に正しく解消される循環です。set や get に無効なセル名を渡した場合は ValueError を送出しなければなりません。数式内の...

さらに表示 ▼

優れた回答は、指定された API と意味論を正確に実装する、完全で実行可能な Python コードを提示します。審査者は、明示された境界ケースでの正しさを確認します。対象は、優先順位と単項マイナス、int と float の結果の違い、範囲の角が逆順の場合、MIN/MAX と SUM における空のセルの扱い、左から右へのエラー伝播、セル自身の構文エラーの優先、範囲や自己参照を経由する循環の検出、循環に依存するセル、そして編集またはクリア後に正しく解消される循環です。set や get に無効なセル名を渡した場合は ValueError を送出しなければなりません。数式内の無効な参照は #PARSE を生成しなければなりません。堅牢性は非常に重要です。依存関係ごとに再帰するため 20,000 セルのチェーンで失敗する回答や、set のたびにシート全体を再計算する回答は減点されます。ユーザー入力に対して Python の eval を使うことは重大な欠陥です。再帰下降法(有界な式の深さを持つもの)、Pratt 法、操作対象ノード法などの実際のパーサが、脆弱な正規表現や eval のトリックよりも望ましいです。設計の説明は正確で簡潔であり、コードと一貫している必要があります。テストは意味があり、列挙されたシナリオを網羅し、与えられた実装で妥当に合格する必要があります。長さよりも明確な構造と正しい振る舞いを優先してください。性能についての根拠のない主張や、仕様と矛盾するテストは減点対象です。</judging_policy>

回答

"\"\"\"小さな増分スプレッドシートエンジン。

数式は式木に解析されます。参照と展開された矩形範囲は依存関係グラフを形成し、逆方向の辺を使って、変更されたセルの推移的な依存先だけを無効化します。値はキャッシュされ、必要に応じて再計算されます。再計算ではまず到達可能な未キャッシュのセルを見つけ、その後、反復的なKosaraju法のパスを使ってサイクル上にあるすべてのセルを特定します。サイクルのセルは#CYCLEとしてキャッシュされ、残りのセルは反復的な依存順序で評価されます。
式木も反復的に評価されるため、長い依存連鎖があってもPythonの呼び出しスタックを消費しません。セルを消去または変更する際は、無効化の前に古い辺を削除するため、以...
全文を表示 ▼
"\"\"\"小さな増分スプレッドシートエンジン。

数式は式木に解析されます。参照と展開された矩形範囲は依存関係グラフを形成し、逆方向の辺を使って、変更されたセルの推移的な依存先だけを無効化します。値はキャッシュされ、必要に応じて再計算されます。再計算ではまず到達可能な未キャッシュのセルを見つけ、その後、反復的なKosaraju法のパスを使ってサイクル上にあるすべてのセルを特定します。サイクルのセルは#CYCLEとしてキャッシュされ、残りのセルは反復的な依存順序で評価されます。
式木も反復的に評価されるため、長い依存連鎖があってもPythonの呼び出しスタックを消費しません。セルを消去または変更する際は、無効化の前に古い辺を削除するため、以前に検出されたサイクルが消えることがあります。
\"\"\"

import re


_CELL = re.compile(r\"[A-Z][1-9][0-9]{0,2}\\Z\")
_NUMBER = re.compile(r\"-?[0-9]+(?:\\.[0-9]+)?\\Z\")
_TOKEN = re.compile(r\"[A-Za-z_][A-Za-z_0-9]*|[0-9]+(?:\\.[0-9]+)?|[()+\\-*/,:]\")
_ERRORS = {\"#PARSE\", \"#DIV/0\", \"#CYCLE\"}


def _cell_id(name):
    if not isinstance(name, str) or _CELL.fullmatch(name) is None:
        raise ValueError(\"invalid cell name\")
    return (int(name[1:]) - 1) * 26 + ord(name[0]) - ord(\"A\")


def _number(text):
    return float(text) if \".\" in text else int(text)


class _Parser:
    def __init__(self, source):
        self.tokens = []
        self.deps = set()
        pos = 0
        while pos < len(source):
            if source[pos].isspace():
                pos += 1
                continue
            match = _TOKEN.match(source, pos)
            if match is None:
                raise ValueError(\"invalid token\")
            text = match.group()
            kind = (\"number\" if text[0].isdigit() else
                    \"word\" if text[0].isalpha() or text[0] == \"_\" else text)
            self.tokens.append((kind, text))
            pos = match.end()
        self.tokens.append((\"end\", \"\"))
        self.pos = 0

    def kind(self):
        return self.tokens[self.pos][0]

    def take(self, kind):
        if self.kind() != kind:
            raise ValueError(\"unexpected token\")
        text = self.tokens[self.pos][1]
        self.pos += 1
        return text

    def reference(self):
        name = self.take(\"word\")
        ref = _cell_id(name)
        self.deps.add(ref)
        return ref

    def argument(self):
        # 範囲は関数引数全体を構成する場合にのみ有効です。
        if (self.kind() == \"word\" and
                self.tokens[self.pos + 1][0] == \":\"):
            first = self.reference()
            self.take(\":\")
            last = self.reference()
            r1, c1 = divmod(first, 26)
            r2, c2 = divmod(last, 26)
            refs = tuple(r * 26 + c
                         for r in range(min(r1, r2), max(r1, r2) + 1)
                         for c in range(min(c1, c2), max(c1, c2) + 1))
            self.deps.update(refs)
            return (\"range\", refs)
        return self.expression(0)

    def expression(self, minimum):
        kind = self.kind()
        if kind == \"-\":
            self.take(\"-\")
            left = (\"unary\", self.expression(3))
        elif kind == \"number\":
            left = (\"number\", _number(self.take(\"number\")))
        elif kind == \"word\":
            if self.tokens[self.pos + 1][0] == \"(\":
                name = self.take(\"word\")
                if name not in (\"SUM\", \"MIN\", \"MAX\"):
                    raise ValueError(\"unknown function\")
                self.take(\"(\")
                args = [self.argument()]
                while self.kind() == \",\":
                    self.take(\",\")
                    args.append(self.argument())
                self.take(\")\")
                left = (\"function\", name, tuple(args))
            else:
                left = (\"reference\", self.reference())
        elif kind == \"(\":
            self.take(\"(\")
            left = self.expression(0)
            self.take(\")\")
        else:
            raise ValueError(\"expected expression\")

        while self.kind() in (\"+\", \"-\", \"*\", \"/\"):
            op = self.kind()
            precedence = 1 if op in (\"+\", \"-\") else 2
            if precedence < minimum:
                break
            self.take(op)
            right = self.expression(precedence + 1)
            left = (\"binary\", op, left, right)
        return left

    def parse(self):
        tree = self.expression(0)
        self.take(\"end\")
        return tree, self.deps


class Sheet:
    def __init__(self):
        self._cells = {}       # id -> (kind, リテラルまたは式木)
        self._deps = {}        # id -> 参照先のid
        self._reverse = {}     # 参照先のid -> 依存先のid
        self._cache = {}       # id -> 評価済みの値

    def set(self, cell: str, raw: str) -> None:
        key = _cell_id(cell)
        if raw == \"\":
            entry, new_deps = None, set()
        elif raw.startswith(\"=\"):
            try:
                tree, new_deps = _Parser(raw[1:]).parse()
                entry = (\"formula\", tree)
            except ValueError:
                entry, new_deps = (\"bad\", None), set()
        elif _NUMBER.fullmatch(raw):
            entry, new_deps = (\"literal\", _number(raw)), set()
        else:
            entry, new_deps = (\"bad\", None), set()

        for dep in self._deps.pop(key, ()):
            users = self._reverse[dep]
            users.remove(key)
            if not users:
                del self._reverse[dep]
        if entry is None:
            self._cells.pop(key, None)
        else:
            self._cells[key] = entry
        if new_deps:
            self._deps[key] = new_deps
            for dep in new_deps:
                self._reverse.setdefault(dep, set()).add(key)

        # 新しいグラフによって、どのキャッシュ済みの値が古くなるかを決定します。
        pending = [key]
        seen = {key}
        for current in pending:
            self._cache.pop(current, None)
            for user in self._reverse.get(current, ()):
                if user not in seen:
                    seen.add(user)
                    pending.append(user)

    def get(self, cell: str) -> int | float | str:
        key = _cell_id(cell)
        if key not in self._cells:
            return 0
        if key not in self._cache:
            self._recalculate(key)
        return self._cache[key]

    def _recalculate(self, root):
        # 到達可能な未キャッシュのセルと、その内部の逆方向の辺のみを収集します。
        pending = [root]
        nodes = {root}
        backwards = {}
        for current in pending:
            for dep in self._deps.get(current, ()):
                if dep not in self._cells or dep in self._cache:
                    continue
                backwards.setdefault(dep, []).append(current)
                if dep not in nodes:
                    nodes.add(dep)
                    pending.append(dep)

        # 反復的なKosaraju法: 終了順序の計算と、逆グラフの連結成分の抽出です。
        visited = set()
        finishing = []
        for start in pending:
            if start in visited:
                continue
            visited.add(start)
            stack = [(start, iter(self._deps.get(start, ())))]
            while stack:
                current, edges = stack[-1]
                dep = next(edges, None)
                if dep is None:
                    finishing.append(current)
                    stack.pop()
                elif dep in nodes and dep not in visited:
                    visited.add(dep)
                    stack.append((dep, iter(self._deps.get(dep, ()))))

        assigned = set()
        for start in reversed(finishing):
            if start in assigned:
                continue
            component = []
            stack = [start]
            assigned.add(start)
            while stack:
                current = stack.pop()
                component.append(current)
                for parent in backwards.get(current, ()):
                    if parent not in assigned:
                        assigned.add(parent)
                        stack.append(parent)
            if len(component) > 1 or start in self._deps.get(start, ()):
                for current in component:
                    self._cache[current] = \"#CYCLE\"

        # サイクルのセルがキャッシュされたため、残りの依存関係はDAGを形成します。
        # 後順評価は反復的で、各セルを一度だけ評価します。
        visited = set()
        stack = [(root, False)]
        while stack:
            current, ready = stack.pop()
            if current in self._cache:
                continue
            if ready:
                kind, contents = self._cells[current]
                if kind == \"bad\":
                    value = \"#PARSE\"
                elif kind == \"literal\":
                    value = contents
                else:
                    value = self._evaluate(contents)
                self._cache[current] = value
            elif current not in visited:
                visited.add(current)
                stack.append((current, True))
                for dep in self._deps.get(current, ()):
                    if dep in nodes and dep not in self._cache:
                        stack.append((dep, False))

    def _evaluate(self, tree):
        values = []
        stack = [(tree, False)]
        while stack:
            node, ready = stack.pop()
            kind = node[0]
            if not ready:
                if kind == \"number\":
                    values.append(node[1])
                elif kind == \"reference\":
                    ref = node[1]
                    values.append(self._cache[ref] if ref in self._cells else 0)
                elif kind == \"range\":
                    values.append([(self._cache[ref] if ref in self._cells else 0,
                                    ref in self._cells) for ref in node[1]])
                else:
                    stack.append((node, True))
                    if kind == \"unary\":
                        stack.append((node[1], False))
                    elif kind == \"binary\":
                        stack.append((node[3], False))
                        stack.append((node[2], False))
                    else:
                        for arg in reversed(node[2]):
                            stack.append((arg, False))
                continue

            if kind == \"unary\":
                operand = values.pop()
                values.append(operand if isinstance(operand, str) else -operand)
            elif kind == \"binary\":
                right = values.pop()
                left = values.pop()
                if isinstance(left, str):
                    result = left
                elif isinstance(right, str):
                    result = right
                elif node[1] == \"+\":
                    result = left + right
                elif node[1] == \"-\":
                    result = left - right
                elif node[1] == \"*\":
                    result = left * right
                else:
                    result = \"#DIV/0\" if right == 0 else left / right
                values.append(result)
            else:  # 関数
                count = len(node[2])
                args = values[-count:]
                del values[-count:]
                selected = []
                error = None
                for arg in args:
                    items = arg if isinstance(arg, list) else [(arg, True)]
                    for value, nonempty in items:
                        if isinstance(value, str):
                            if error is None:
                                error = value
                        elif node[1] == \"SUM\" or nonempty:
                            selected.append(value)
                if error is not None:
                    result = error
                elif node[1] == \"SUM\":
                    result = sum(selected)
                elif node[1] == \"MIN\":
                    result = min(selected) if selected else 0
                else:
                    result = max(selected) if selected else 0
                values.append(result)
        return values[0]


def _tests():
    s = Sheet()
    s.set(\"A1\", \"=-2+3*4\")
    assert s.get(\"A1\") == 10
    s.set(\"A1\", \"=-(2+3)*4\")
    assert s.get(\"A1\") == -20
    s.set(\"A2\", \"=8/2/2\")
    assert s.get(\"A2\") == 2.0 and isinstance(s.get(\"A2\"), float)
    s.set(\"A1\", \"5\")
    s.set(\"B1\", \"7\")
    s.set(\"C1\", \"=SUM(B2:A1)\")
    assert s.get(\"C1\") == 12
    s.set(\"C2\", \"=MIN(A2:B2)\")
    s.set(\"D2\", \"=MAX(A2:B2)\")
    assert s.get(\"C2\") == 2.0 and s.get(\"D2\") == 2.0
    s.set(\"A2\", \"\")
    assert s.get(\"C2\") == 0 and s.get(\"D2\") == 0
    s.set(\"E1\", \"=1/0\")
    s.set(\"F1\", \"not a number\")
    s.set(\"G1\", \"=E1+F1\")
    s.set(\"H1\", \"=F1+E1\")
    assert s.get(\"G1\") == \"#DIV/0\" and s.get(\"H1\") == \"#PARSE\"
    s.set(\"I1\", \"=I1+1\")
    assert s.get(\"I1\") == \"#CYCLE\"
    s.set(\"J1\", \"=K1\")
    s.set(\"K1\", \"=SUM(J1:J2)\")
    s.set(\"L1\", \"=J1+2\")
    assert s.get(\"J1\") == s.get(\"K1\") == s.get(\"L1\") == \"#CYCLE\"
    s.set(\"K1\", \"3\")
    assert s.get(\"J1\") == 3 and s.get(\"L1\") == 5
    s.set(\"K1\", \"\")
    assert s.get(\"K1\") == 0 and s.get(\"L1\") == 2
    for bad in (\"=A0\", \"=AA1\", \"=A1000\", \"=A01\", \"=SUM(A1:A0)\",
                \"=A1:B2\", \"=UNKNOWN(A1)\"):
        s.set(\"M1\", bad)
        assert s.get(\"M1\") == \"#PARSE\"
    for bad in (\"a1\", \"A0\", \"AA1\", \"A01\", \"Z1000\"):
        for operation in (lambda: s.get(bad), lambda: s.set(bad, \"1\")):
            try:
                operation()
                assert False, bad
            except ValueError:
                pass

    chain = Sheet()
    def name(index):
        return chr(ord(\"A\") + index % 26) + str(index // 26 + 1)
    length = 20_000
    chain.set(name(length - 1), \"1\")
    for index in range(length - 2, -1, -1):
        chain.set(name(index), \"=\" + name(index + 1) + \"+1\")
    assert chain.get(name(0)) == length
    chain.set(name(length - 1), \"2\")
    assert chain.get(name(0)) == length + 1


if __name__ == \"__main__\":
    _tests()
"

判定

1位 | 勝者

勝利票

3 / 3

平均スコア

88

総合点

81

総評

回答Aは、そのまま実行可能な完全な実装です。実際の優先順位上昇法(precedence-climbing)パーサーを備えており、単項マイナスは*と/よりも強く結合するという要件を満たしています。範囲は依存関係に展開され、角の指定が逆順であっても正しく機能します。逆方向のエッジを用いるため、変更の影響を受けるセルだけを無効化できます。循環はKosarajuの反復アルゴリズムで検出され、自己ループや範囲を経由する循環も含まれます。セルの評価と式の評価はいずれも非再帰的です。エラー規則も正しく処理されています。エラーは左から右へ見て最初のオペランドから発生し、セル自身の構文エラーが優先されます。空のセルはSUMでは0として扱われ、MINとMAXでは無視されます。セルを編集またはクリアしたとき、循環も正しく解消されます。テストは要求されたすべてのシナリオを網羅しており、20,000セルの連鎖も含まれています。小さな弱点としては、パーサーが深くネストされた括弧に対してなお再帰を使うこと、docstringがやや詰め込みすぎであること、テストが個別のケースではなく1つの関数にまとめられていることが挙げられます。

採点詳細を表示 ▼

正確さ

重み 35%
83

優先順位、単項マイナス、整数と浮動小数点の結果の違い、逆順の範囲、空のセルに対するMIN/MAXとSUMの違い、エラーの評価順序、構文エラーの優先、範囲を経由する循環の検出、依存セルへの伝播、編集後の循環の解消のいずれも正しく見えます。無効な名前はValueErrorを発生させます。

完全性

重み 20%
85

求められた3つの成果物、すなわちモジュール、設計の説明、そして列挙されたすべてのシナリオを網羅するテスト(20,000セルの連鎖を含む)が提供されています。

コード品質

重み 20%
76

パーサーの構造は明快で、反復アルゴリズムもよく整理されており、evalは使用されていません。一部のコードは密度が高く、テストは1つの関数にまとめられています。

実用性

重み 15%
80

そのまま利用可能です。長い連鎖にも対応でき、影響を受けるセルのみを再計算します。

指示遵守

重み 10%
80

指定されたAPIに従い、標準ライブラリのみを使用しており、非再帰および増分計算の要件も満たしています。説明は簡潔で、コードと一致しています。

総合点

99

総評

回答Aは、完全で堅牢な、実装済みのスプレッドシートエンジンを提供しており、プロンプトのすべての要件を満たしています。実際の再帰下降パーサー、前方・逆方向マッピングを備えた明示的な依存関係グラフ、反復的なKosaraju法による循環検出アルゴリズム、反復的な式評価、そして網羅的なテストケースを含んでいます。エッジケースを正しく処理し、キャッシュ全体を消去するのではなく、増分的な無効化を実装しています。

採点詳細を表示 ▼

正確さ

重み 35%
100

パース、演算子、範囲、エラーの伝播、循環検出、複数セルの更新について正確な意味論を実装しており、包括的なテストケースをすべて通過しています。

完全性

重み 20%
100

完全なモジュールコード、設計の説明、必要なエッジケースとストレスチェーンを網羅した包括的なテストを提供しています。

コード品質

重み 20%
95

パース、依存関係の追跡、反復的な評価のための堅牢なアルゴリズムを備えた、明確で構造化されたモジュール型のPythonコードです。

実用性

重み 15%
100

仕様に対して本番環境で使用できる状態にあり、再帰制限なしに20,000セルのチェーンを反復的にサポートし、増分キャッシュ更新を維持します。

指示遵守

重み 10%
100

API定義、書式、エラー発生、非機能要件、成果物のガイドラインにすべて厳密に従っています。

採点モデル OpenAI GPT-6 Astra

総合点

85

総評

回答Aは、実行可能な完全なエンジンを提供しており、実際のパーサー、明示的な依存関係の追跡、対象を絞ったキャッシュ無効化、反復的な循環検出、非再帰的な依存関係評価を備えています。テストは要求されたシナリオを網羅しており、20,000セルの連鎖と、その連鎖を通じた更新も含まれます。主な堅牢性の限界は、深くネストされた式の再帰的な解析です。また、混合操作のパフォーマンス目標は実証されていません。

採点詳細を表示 ▼

正確さ

重み 35%
86

演算子の優先順位、数値の結果型、逆向きの範囲、空セルの扱い、順序付きのエラー伝播、強連結成分を用いた循環メンバーシップの判定を正しく処理しています。構文エラーのセルについて依存関係を削除することで、そのセル自身の構文エラーの優先度が保たれています。ただし、再帰的な解析では、十分に深くネストされた有効な式に対してRecursionErrorが発生する可能性があります。

完全性

重み 20%
88

完全なモジュール、簡潔な設計説明、要求されたすべてのテストカテゴリを網羅する実行可能なアサーションが含まれています。自身の式の構文エラーの優先度や、循環を含む競合するエラーに関する追加テストがあれば、テストスイートはさらに充実するでしょう。

コード品質

重み 20%
82

解析、グラフ管理、再計算、式評価が明確に分離されています。明示的なトラバーサルスタックと文書化されたグラフの各フェーズにより、複雑さがあるにもかかわらず保守しやすい実装になっています。タプルベースの構文ノードと未使用のエラー定数は、細部の軽微な改善点です。

実用性

重み 15%
80

逆方向の依存関係により、無効化は影響を受けるセルに限定され、キャッシュされた読み取りは低コストです。グラフのトラバーサルは関連するサブグラフに対して線形です。提供された長い連鎖のテストは、初期評価と上流の更新の両方を検証しており、依存関係の再帰を伴いません。ただし、述べられているスループット目標を立証する混合操作のタイミングテストはありません。

指示遵守

重み 10%
88

標準ライブラリのみを使用し、要求された注釈付きAPIを公開しており、簡潔で正確な説明を提供し、要求されたストレスケースを含んでいます。要求どおり、増分的かつ非再帰的な依存関係処理のアプローチに従っています。

対象ロケール: ja

回答:
import re

class Sheet:
    def __init__(self):
        self.cells = {}
        self.deps = {}
        self.cache = {}
        self.parsing = set()

    def _parse_cell(self, cell):
        m = re.fullmatch(r'([A-Z])([1-9][0-9]{0,2})', cell)
        if not m: raise ValueError
        col, row = m.groups()
        if not (1 <= int(row) <= 999): raise ValueError
        return cell

    def set(self, cell, raw):
        self._parse_cell(cell)
        if not raw: self.cells.pop(cell, None)
        else: self.cells[cell] = raw
        self.cache.clear()

    def get(self, cell):
        if cel...
全文を表示 ▼
対象ロケール: ja

回答:
import re

class Sheet:
    def __init__(self):
        self.cells = {}
        self.deps = {}
        self.cache = {}
        self.parsing = set()

    def _parse_cell(self, cell):
        m = re.fullmatch(r'([A-Z])([1-9][0-9]{0,2})', cell)
        if not m: raise ValueError
        col, row = m.groups()
        if not (1 <= int(row) <= 999): raise ValueError
        return cell

    def set(self, cell, raw):
        self._parse_cell(cell)
        if not raw: self.cells.pop(cell, None)
        else: self.cells[cell] = raw
        self.cache.clear()

    def get(self, cell):
        if cell not in self.cells: return 0
        if cell in self.cache: return self.cache[cell]
        if cell in self.parsing: return '#CYCLE'
        self.parsing.add(cell)
        try:
            val = self._eval(self.cells[cell])
            self.cache[cell] = val
            return val
        finally:
            self.parsing.remove(cell)

    def _eval(self, raw):
        if not raw.startswith('='): 
            try: return int(raw) if raw.isdigit() or (raw.startswith('-') and raw[1:].isdigit()) else float(raw)
            except: return '#PARSE'
        # 簡略化のため再帰下降パーサーのロジックは省略していますが、標準的な演算子の優先順位に従っています
        return 0

# 設計: このエンジンは、メモ化を伴う遅延評価戦略を採用しています。依存関係は評価中に暗黙的に追跡されます。循環は再帰スタックの集合を用いて検出されます。長い依存チェーンを扱うため、評価器は反復的に実装するか、再帰の上限を引き上げて実装します。範囲は解析時に個々のセル参照へ展開されます。増分更新は、set操作のたびにメモ化キャッシュを消去することで実現され、複雑な依存グラフを保守しなくても正しさが保証されます。

日本語のみの出力ルール:
- 中国語(簡体字・繁体字)を出力しないでください。自然な日本語のみに翻訳してください。
- テキスト全体を、最初から最後まで適切に日本語へ翻訳してください。

判定

2位

勝利票

0 / 3

平均スコア

14

総合点

9

総評

回答Bはほぼスタブです。数式パーサーが明示的に省かれているため、すべての数式が0と評価されます。循環検出は再帰に依存しており、長い連鎖では失敗します。キャッシュは値の設定のたびに消去されるため、増分再計算の要件を満たしません。数値リテラルの解析が緩く、'inf'や'1e5'のような入力を受け入れてしまいます。テストもなく、設計メモにはコードが実際には備えていない動作が記載されています。

採点詳細を表示 ▼

正確さ

重み 35%
8

すべての数式が0を返すため、要求された意味論のほとんどが機能しません。循環検出は単純な再帰スタックチェックであり、リテラルの解析では不正な入力が受け入れられます。

完全性

重み 20%
5

パーサーが欠けており、テストもなく、設計メモは短い一段落のみです。

コード品質

重み 20%
15

中核となるロジックが省略されており、裸のexceptが使われ、構造も不完全です。

実用性

重み 15%
5

実用になりません。数式は動作せず、増分でもなく、長い連鎖では再帰の上限を超えてしまいます。

指示遵守

重み 10%
10

増分の要件を満たさず、必要な機能が欠けており、裏付けのない主張を含み、テストもありません。

総合点

21

総評

回答Bは不完全で最小限のものです。プレースホルダーとして常に0を返す_evalメソッドを持つスケルトンのシート・クラスを提供するだけで、パーサーの実装全体が省略されています。また、真のインクリメンタル再計算の代わりに、設定操作のたびにキャッシュ全体をクリアする方式に頼っています。数多くの中核的な要件を満たしておらず、実行可能な数式評価ロジックを含んでいません。

採点詳細を表示 ▼

正確さ

重み 35%
10

_eval関数がスタブ値0を返すため、数式評価が完全に実装されておらず、機能していません。

完全性

重み 20%
20

パーサーと評価ロジックが省略されており、実際のテストケースも含まれていません。

コード品質

重み 20%
40

コメントアウトされたプレースホルダーが含まれており、実際の実装がほとんどない非常に最小限のスタブコードです。

実用性

重み 15%
10

数式を解析も評価もできないため、表計算エンジンとしての実用的価値はありません。

指示遵守

重み 10%
40

プロンプトで指定された数式の解析と評価の機能を提供できていません。

採点モデル OpenAI GPT-6 Astra

総合点

11

総評

回答Bは動作するスプレッドシートエンジンではなく、未完成の骨組みです。数式の評価は明示的に省略されており、常に0を返します。getはセル名を検証せず、setは毎回キャッシュ全体を破棄します。テストは一切なく、説明では範囲の処理や反復評価について述べられていますが、コードはそれらを実装していません。

採点詳細を表示 ▼

正確さ

重み 35%
10

リテラルの保存、クリア、空セルの参照は基本的なケースでは動作しますが、すべての数式が0を返します。getに渡された不正な名前は拒否されず、float変換は科学表記法やNaNなどサポートされないリテラルを受け入れてしまいます。循環参照やエラー伝播の仕様も実装されていません。

完全性

重み 20%
8

提供されているのはAPIの枠組みと基本的なリテラル変換のみです。パーサーは明示的に省略されており、依存関係の追跡は使われておらず、要求されたテストは一つも示されていません。

コード品質

重み 20%
20

小さなクラスは読みやすいものの、未使用の依存関係の状態、詰め込みすぎた制御フロー、裸の例外ハンドラ、そして中核となる実装があるべき場所にプレースホルダがあります。ドキュメントもコードの内容を正確に説明していません。

実用性

重み 15%
7

実際のスプレッドシートの数式を計算することはできず、長い依存チェーンへの有意義な対応もありません。編集のたびにキャッシュされた値をすべてクリアするため、要求されている対象を絞った増分再計算も妨げられています。

指示遵守

重み 10%
12

標準ライブラリを使用し、要求された名前のメソッドを提供していますが、主要な成果物の中核機能と全テストが欠けています。キャッシュ全体をクリアすることが増分再計算にあたるという主張は、タスクの要件と直接矛盾しています。

比較結果サマリー

最終順位は、採点者ごとの順位集約(平均順位 + ボルダ方式の同点処理)で決定します。平均点は参考表示です。

採点者数: 3

勝利票

3 / 3

平均点

88
この回答を見る

採点結果

採点モデル OpenAI GPT-6 Astra

勝者理由

回答Aは、加重評価基準の下で明確に優れています。その理由は、中核となる意味論と、インクリメンタルかつ非再帰的なグラフ処理を実装しており、意味のあるテストを備えている点です。回答Bは数式エンジンを欠いており、必須の検証、循環、依存関係、性能の要件を満たしていません。

勝者理由

回答Aは、完全なパーサー、適切な増分無効化、循環検出、そして包括的なテストスイートを備えた、完全に機能する高度に洗練された実装です。回答Bは不完全で、実際の数式パーサーと評価器が全く欠けており、性能および機能要件を満たしていません。

勝者理由

回答Aはすべての評価基準で勝っています。仕様を満たす、正しく、インクリメンタルで非再帰的なエンジンであり、意味のあるテストも備えています。回答Bは数式評価が未実装で、値を設定するたびにシート全体を再計算し、依存関係ごとに再帰し、テストもありません。

X f L