# Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
# Copyright (c) 2021 Dan Hendrycks
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
数学答案验证模块，用于判断模型输出的数学答案是否正确。

验证流程（两层递进）：
  第 1 层: Hendrycks 标准字符串归一化 + 精确匹配
           来自 lm-evaluation-harness，处理 LaTeX 格式差异（\\frac, \\sqrt 等）
  第 2 层: SymPy 代数等价性回退
           来自 PRIME/ToRA 评分器，通过符号简化和数值比较处理代数等价表达式

答案提取支持三种格式：
  1. \\boxed{...}     — Hendrycks MATH 标准格式
  2. #### <answer>    — GSM8K 格式
  3. ### ANSWER: ...   — 自定义格式

用法：
    from functions.reward.math_verify import verify_math_answer
    is_correct = verify_math_answer(model_output, ground_truth)  # 返回 1.0 或 0.0
"""

import contextlib
import re
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
from math import isclose

import sympy
from sympy.parsing.latex import parse_latex
from sympy.parsing.sympy_parser import (
    convert_xor,
    implicit_multiplication_application,
    parse_expr,
    standard_transformations,
)

# 给 parse_expr 装上隐式乘法和 ^→** 转换:
#   - implicit_multiplication_application: 让 "(x-2)(x+2)" / "2x" / "x(y+1)" 直接可解析
#   - convert_xor: 让数学常用的 x^2 被识别为幂运算而非 Python 的位异或
# 这两项让大部分多项式表达式不再依赖 parse_latex(及 antlr4)就能等价判定。
_SYMPY_TRANSFORMATIONS = standard_transformations + (
    implicit_multiplication_application,
    convert_xor,
)

# 全局线程池用于给 SymPy 调用加超时(SIGALRM 仅在主线程可用,且并发下会互相干扰)。
# max_workers 设小一些,避免 SymPy 卡死时占满线程;每个调用本身也有超时上限。
_SYMPY_EXECUTOR = ThreadPoolExecutor(max_workers=8, thread_name_prefix="sympy-timeout")


def _run_with_timeout(func, timeout: float):
    """在独立线程里跑 func(),超时返回 None;线程级超时,daemon 线程会被丢弃。

    注意:Python 没有安全的线程取消机制,超时后 SymPy 线程实际仍在跑,
    但调用方拿到 None 立即继续,不会阻塞 reward 主流程。线程池容量保护了 worker。
    """
    future = _SYMPY_EXECUTOR.submit(func)
    try:
        return future.result(timeout=timeout)
    except FutureTimeoutError:
        return None
    except Exception:
        return None


# ---------------------------------------------------------------------------
#  答案提取 — 支持 \boxed{}, ####, ### ANSWER: 三种格式
# ---------------------------------------------------------------------------

def extract_answer(text: str) -> str:
    """从模型输出中提取最终答案，按优先级尝试多种格式。

    提取优先级：
      1. \\boxed{...} — Hendrycks MATH 标准
      2. #### <answer> — GSM8K 风格
      3. ### ANSWER: <answer> ### — 自定义标记
      4. 回退：返回最后一行非空文本

    Args:
        text: 模型的完整输出文本。

    Returns:
        str: 提取到的答案字符串。
    """
    # 1) \boxed{...} — standard hendrycks_math
    boxed = _last_boxed_only_string(text)
    if boxed is not None:
        return _remove_boxed(boxed)

    # 2) #### <answer>  — GSM8K style
    m = re.search(r"####\s*(.+?)(?:\s*$)", text, re.DOTALL)
    if m:
        return m.group(1).strip()

    # 3) ### ANSWER: <answer> ###
    m = re.search(r"###\s*ANSWER:\s*(.+?)(?:\s*###|$)", text, re.DOTALL)
    if m:
        return m.group(1).strip()

    # 4) fallback: return last non-empty line
    for line in reversed(text.strip().split("\n")):
        line = line.strip()
        if line:
            return line
    return text.strip()


def extract_answer_by_format(text: str, dataset: str = "") -> str | None:
    """严格按 \\boxed{} 格式提取答案（用于格式奖励判定）。

    所有数学数据集统一使用 \\boxed{} 作为目标答案格式。
    仅当文本中存在 \\boxed{} 且内容非空时返回答案，否则返回 None。

    Args:
        text: 模型输出文本。
        dataset: 数据集名称（预留扩展，当前未使用）。

    Returns:
        str | None: 提取到的答案字符串，或 None（格式不符）。
    """
    boxed = _last_boxed_only_string(text)
    if boxed is None:
        return None
    ans = _remove_boxed(boxed)
    return ans if ans.strip() else None


def _last_boxed_only_string(string: str):
    """定位文本中最后一个 \\boxed{...} 或 \\fbox{...} 并返回完整匹配。

    通过大括号计数来正确处理嵌套大括号的情况。
    """
    idx = string.rfind("\\boxed")
    if "\\boxed " in string:
        return "\\boxed " + string.split("\\boxed ")[-1].split("$")[0]
    if idx < 0:
        idx = string.rfind("\\fbox")
        if idx < 0:
            return None
    i = idx
    num_left_braces_open = 0
    right_brace_idx = None
    while i < len(string):
        if string[i] == "{":
            num_left_braces_open += 1
        if string[i] == "}":
            num_left_braces_open -= 1
            if num_left_braces_open == 0:
                right_brace_idx = i
                break
        i += 1
    if right_brace_idx is None:
        return None
    return string[idx : right_brace_idx + 1]


def _remove_boxed(s: str) -> str:
    """从 '\\boxed{content}'、'\\boxed content' 或 '\\fbox{content}' 中提取 content。

    若输入不符合任何已知格式则返回原串(不再 assert),避免在 reward 路径抛异常。
    """
    if s.startswith("\\boxed "):
        return s[len("\\boxed "):]
    for left in ("\\boxed{", "\\fbox{"):
        if s.startswith(left) and s.endswith("}"):
            return s[len(left): -1]
    return s


# ---------------------------------------------------------------------------
#  第 1 层: Hendrycks 标准字符串归一化（来自 lm-evaluation-harness）
# ---------------------------------------------------------------------------

def _fix_fracs(string: str) -> str:
    """修复不规范的 \\frac 写法，统一为 \\frac{a}{b}。

    覆盖三种异常：
      - \\frac12      → \\frac{1}{2}   （两个参数都无括号）
      - \\frac1{2}    → \\frac{1}{2}   （第一个参数无括号）
      - \\frac{1}2    → \\frac{1}{2}   （第二个参数无括号；Hendrycks 原版漏掉）
    """
    substrs = string.split("\\frac")
    new_str = substrs[0]
    if len(substrs) <= 1:
        return new_str
    for substr in substrs[1:]:
        new_str += "\\frac"
        if not substr:
            continue
        if substr[0] == "{":
            # 第一参数已带花括号，定位其闭合位置,再判断第二参数是否需要补括号
            depth = 0
            end = -1
            for j, ch in enumerate(substr):
                if ch == "{":
                    depth += 1
                elif ch == "}":
                    depth -= 1
                    if depth == 0:
                        end = j
                        break
            if end < 0 or end + 1 >= len(substr) or substr[end + 1] == "{":
                new_str += substr
                continue
            # 形如 \frac{a}b... → \frac{a}{b}...
            new_str += substr[: end + 1] + "{" + substr[end + 1] + "}" + substr[end + 2:]
        else:
            try:
                assert len(substr) >= 2
            except Exception:
                return string
            a, b = substr[0], substr[1]
            rest = substr[2:] if len(substr) > 2 else ""
            if b != "{":
                new_str += "{" + a + "}{" + b + "}" + rest
            else:
                new_str += "{" + a + "}" + b + rest
    return new_str


def _fix_a_slash_b(string: str) -> str:
    """将 "a/b" 形式的分数转换为 \\frac{a}{b}，如 "1/2" → "\\frac{1}{2}"。"""
    if len(string.split("/")) != 2:
        return string
    a_str, b_str = string.split("/")
    try:
        a, b = int(a_str), int(b_str)
        assert string == f"{a}/{b}"
        return f"\\frac{{{a}}}{{{b}}}"
    except Exception:
        return string


_TEXT_UNIT_RE = re.compile(r'\\text\{\s+[^{}]*\}')
_TEXT_LABEL_RE = re.compile(r'\\text\{([^{}]*)\}')


def _remove_right_units(string: str) -> str:
    """规范 LaTeX 的 \\text{} 标注。

    两种用法在 MATH 数据集都常见,分别处理:
      - \\text{ X}  (内容以空白起始) → 视为单位标注,整体删除
                                       e.g. "8.24 \\text{ mph}" → "8.24"
      - \\text{X}   (无前导空白)     → 视为标签包裹,只剥外壳保留内容
                                       e.g. "\\text{H, E}"     → "H, E"
    """
    string = _TEXT_UNIT_RE.sub('', string)
    string = _TEXT_LABEL_RE.sub(r'\1', string)
    return string


def _fix_sqrt(string: str) -> str:
    """修复不规范的 \\sqrt 写法。

    覆盖三种异常都规范成 \\sqrt{X}:
      - \\sqrt2    (无括号紧跟参数)        → \\sqrt{2}
      - \\sqrt 3   (空白分隔参数)          → \\sqrt{3}
      - \\sqrt{2}  (本就规范)              → 保持不变
    """
    if "\\sqrt" not in string:
        return string
    splits = string.split("\\sqrt")
    new_string = splits[0]
    for split in splits[1:]:
        # 跳过 \sqrt 与参数之间的空白(如 \sqrt 3 / \sqrt   x)
        stripped = split.lstrip()
        if stripped and stripped[0] != "{":
            new_string += "\\sqrt{" + stripped[0] + "}" + stripped[1:]
        else:
            new_string += "\\sqrt" + split
    return new_string


_PMATRIX_RE = re.compile(
    r'\\begin\{(?:pmatrix|bmatrix|matrix|vmatrix)\}(.+?)\\end\{(?:pmatrix|bmatrix|matrix|vmatrix)\}',
    re.DOTALL,
)


def _pmatrix_to_tuple(string: str) -> str:
    """把 \\begin{pmatrix} a \\\\ b \\end{pmatrix} 这类矩阵规范成元组 (a,b)。

    必须在 strip_string 把 \\\\ 折叠成 \\ 之前调用,因为行分隔符正是 \\\\ 。
    多行 / 多列都按行优先线性化:`\\\\` 分行,`&` 分列。
    """
    def _repl(m):
        body = m.group(1)
        # 先按 \\\\ 切行,再按 & 切列;空白条目过滤
        rows = [r for r in re.split(r'\\\\', body) if r.strip()]
        cells = []
        for r in rows:
            for c in r.split('&'):
                c = c.strip()
                if c:
                    cells.append(c)
        return '(' + ','.join(cells) + ')'
    return _PMATRIX_RE.sub(_repl, string)


def strip_string(string: str) -> str:
    """Hendrycks MATH 标准归一化流程。

    依次处理：矩阵→元组（先于 \\\\ 折叠）、换行符、LaTeX 转义、分数变体（tfrac/dfrac）、
    括号、度数符号、美元符号、单位、百分号、小数点前导零、等号左侧简短变量名、
    根号、空格、分数格式统一。

    Args:
        string: 原始答案字符串。

    Returns:
        str: 归一化后的字符串，可直接用于精确比较。
    """
    # 必须最先做:pmatrix 等用 \\\\ 作为行分隔,会被下面的 \\\\ → \\ 替换破坏
    string = _pmatrix_to_tuple(string)
    string = string.replace("\n", "")
    string = string.replace("\\!", "")
    string = string.replace("\\\\", "\\")
    string = string.replace("tfrac", "frac")
    string = string.replace("dfrac", "frac")
    string = string.replace("\\left", "")
    string = string.replace("\\right", "")
    string = string.replace("^{\\circ}", "")
    string = string.replace("^\\circ", "")
    string = string.replace("\\$", "")
    # 近似符号 \approx 在归一化中视为等号:仅作为前缀提示,不影响数值比较
    string = string.replace("\\approx", "")
    string = _remove_right_units(string)
    string = string.replace("\\\\%", "")
    string = string.replace("\\%", "")
    string = string.replace(" .", " 0.")
    string = string.replace("{.", "{0.")
    if len(string) == 0:
        return string
    if string[0] == ".":
        string = "0" + string
    # 若为 "x = value" 形式且变量名 ≤ 2 字符，取等号右侧
    if len(string.split("=")) == 2 and len(string.split("=")[0]) <= 2:
        string = string.split("=")[1]
    string = _fix_sqrt(string)
    string = string.replace(" ", "")
    string = _fix_fracs(string)
    if string == "0.5":
        string = "\\frac{1}{2}"
    string = _fix_a_slash_b(string)
    return string


def is_equiv(str1: str, str2: str) -> bool:
    """Hendrycks 标准等价判定：先归一化再字符串比较。

    Args:
        str1: 第一个答案字符串。
        str2: 第二个答案字符串。

    Returns:
        bool: 归一化后是否完全相同。
    """
    if str1 is None and str2 is None:
        return True
    if str1 is None or str2 is None:
        return False
    try:
        return strip_string(str1) == strip_string(str2)
    except Exception:
        return str1 == str2


# ---------------------------------------------------------------------------
#  第 2 层: SymPy 代数等价性回退
# ---------------------------------------------------------------------------

_BRACKET_PAIRS = {'(': ')', '[': ']', '{': '}'}


def _peel_outer_bracket(s: str) -> str:
    """剥一层"完整包裹整个表达式"的外层括号。

    与 str.strip("[](){}") 不同,这里走深度匹配,只在最外括号确实匹配整个串时才剥。
    避免:
      - "(a)(b)"           被剥成 "a)(b"  (中间已闭合)
      - "(-\\frac{1}{2})"  被剥成 "-\\frac{1}{2"  (尾部 } 误吃)
    """
    s = s.strip()
    if len(s) < 2 or s[0] not in _BRACKET_PAIRS or s[-1] != _BRACKET_PAIRS[s[0]]:
        return s
    depth = 0
    for i, ch in enumerate(s):
        if ch in _BRACKET_PAIRS:
            depth += 1
        elif ch in ')]}':
            depth -= 1
            if depth == 0 and i < len(s) - 1:
                return s
    return s[1:-1]


_THOUSANDS_RE = re.compile(r'^-?\d{1,3}(,\d{3})+(\.\d+)?$')


def _is_digit(s) -> tuple:
    """尝试将字符串解析为浮点数，处理千分位逗号（如 "1,000" 或 "1{,}000"）。

    几个必须挡住的陷阱:
      - `_`: Python 3.6+ float() 当数字分隔符(PEP 515),会把 `2003_6` 解成
            20036,而 MATH 里 `_` 是进制/LaTeX 下标。
      - `,` 在非千分位场景: `3,1,2` 会被 `replace(",", "")` 变成 312,把
            解集当数字。仅当严格匹配千分位形式 `\\d{1,3}(,\\d{3})+(\\.\\d+)?`
            时才剥逗号。

    Returns:
        tuple: (是否为数字, 浮点数值或 None)。
    """
    s_str = str(s)
    if "_" in s_str:
        return False, None
    if "{,}" in s_str:
        try:
            return True, float(s_str.replace("{,}", ""))
        except (ValueError, TypeError):
            return False, None
    if "," in s_str:
        if _THOUSANDS_RE.match(s_str.strip()) is None:
            return False, None
        try:
            return True, float(s_str.replace(",", ""))
        except (ValueError, TypeError):
            return False, None
    try:
        return True, float(s_str)
    except (ValueError, TypeError):
        return False, None


def _try_parse_number(s: str) -> tuple:
    r"""尝试将字符串（含 LaTeX 分数）解析为浮点数。

    在 _is_digit 基础上额外支持：
      - LaTeX 分数:  \frac{3}{2}  →  1.5
      - 带负号分数:  -\frac{3}{2}  →  -1.5
      - 简单分数:    3/2  →  1.5

    Returns:
        tuple: (是否成功, 浮点数值或 None)。
    """
    is_digit, val = _is_digit(s)
    if is_digit:
        return True, val

    # \dfrac / \tfrac 与 \frac 等价,先归一化以让下方 \frac 正则覆盖
    s_clean = s.strip().replace("\\dfrac", "\\frac").replace("\\tfrac", "\\frac")

    # 混合数: \d+\frac{\d+}{\d+} 解释为整数 + 分数
    # MATH 数据集惯例: 2\frac{1}{2} 表示 2½ = 2.5,而非乘法 2 × 1/2 = 1
    # (SymPy parse_latex 会按乘法解析,因此必须在数值短路里直接处理)
    m = re.match(r'^(-?)(\d+)\\frac\{(\d+)\}\{(\d+)\}$', s_clean)
    if m:
        try:
            sign = -1.0 if m.group(1) == '-' else 1.0
            whole = float(m.group(2))
            num = float(m.group(3))
            den = float(m.group(4))
            if den != 0:
                return True, sign * (whole + num / den)
        except (ValueError, ZeroDivisionError):
            pass

    # \frac{a}{b} 或 -\frac{a}{b}
    m = re.match(r'^(-?)\\frac\{([^}]+)\}\{([^}]+)\}$', s_clean)
    if m:
        try:
            sign = -1.0 if m.group(1) == '-' else 1.0
            num = float(m.group(2))
            den = float(m.group(3))
            if den != 0:
                return True, sign * num / den
        except (ValueError, ZeroDivisionError):
            pass

    # a/b 形式
    parts = s_clean.split('/')
    if len(parts) == 2:
        try:
            return True, float(parts[0].strip()) / float(parts[1].strip())
        except (ValueError, ZeroDivisionError):
            pass

    return False, None


def _symbolic_equal(a: str, b: str, tolerance: float = 1e-4, timeout: float = 5.0) -> bool:
    """通过 SymPy 符号简化和数值求值判断代数等价性。

    两步尝试：
      1. 符号简化: simplify(a - b) == 0
      2. 数值比较: float(N(a)) ≈ float(N(b))

    每步用线程池超时保护（默认 5 秒），防止 SymPy 在复杂表达式上卡死。
    （原先用 SIGALRM，但 signal 仅在主线程可用且并发下会互相覆盖。）

    Args:
        a: 第一个数学表达式字符串。
        b: 第二个数学表达式字符串。
        tolerance: 数值比较的相对容差。
        timeout: 每步操作的超时时间（秒）。

    Returns:
        bool: 是否代数等价。
    """
    def _parse(s):
        # 优先 parse_expr(已带 implicit_multiplication + convert_xor),覆盖大多数多项式;
        # parse_latex 兜底 \frac / \sqrt 等 LaTeX 专属语法。
        result = _run_with_timeout(
            lambda: parse_expr(s, transformations=_SYMPY_TRANSFORMATIONS), timeout
        )
        if result is not None:
            return result
        result = _run_with_timeout(lambda: parse_latex(s), timeout)
        if result is not None:
            return result
        return s

    pa, pb = _parse(a), _parse(b)

    # 快路径: expand 对多项式差分秒级出 0,比 simplify 便宜得多
    expanded = _run_with_timeout(lambda: sympy.expand(pa - pb), timeout)
    if expanded is not None:
        try:
            if expanded == 0:
                return True
        except Exception:
            pass

    # 慢路径: 仅在 expand 没 reduce 到 0 时再调 simplify(处理三角/对数等非多项式)
    simplified = _run_with_timeout(lambda: sympy.simplify(pa - pb), timeout)
    if simplified is not None:
        try:
            if simplified == 0:
                return True
        except Exception:
            pass

    # 数值求值兜底
    try:
        if isclose(float(sympy.N(pa)), float(sympy.N(pb)), rel_tol=tolerance):
            return True
    except Exception:
        pass

    return False


def _math_equal(prediction: str, reference: str, tolerance: float = 1e-4) -> bool:
    """综合数值和符号比较，支持元组/坐标等复合格式。

    比较策略（按顺序）：
      1. 字符串精确匹配（含去空格）
      2. 纯数字 → 浮点数比较（兼容百分比：×0.01 / ×100）
      3. 去括号后比较
      4. 逗号分隔的复合答案 → 逐元素递归比较
      5. 回退到 SymPy 符号等价判定

    Args:
        prediction: 模型预测的答案。
        reference: 标准答案。
        tolerance: 数值比较的相对容差。

    Returns:
        bool: 是否等价。
    """
    if not prediction or not reference:
        return False

    pred = prediction.strip().lower()
    ref = reference.strip().lower()

    if pred == ref:
        return True
    if pred.replace(" ", "") == ref.replace(" ", ""):
        return True

    # 数值比较（兼容 LaTeX 分数和百分比表示差异）
    is_pred_num, pred_val = _try_parse_number(pred)
    is_ref_num, ref_val = _try_parse_number(ref)
    if is_pred_num and is_ref_num:
        for factor in [1.0, 0.01, 100.0]:
            with contextlib.suppress(Exception):
                if isclose(ref_val * factor, pred_val, rel_tol=tolerance):
                    return True
        return False

    # 去除外层括号后比较
    pred_stripped = pred.strip("[](){}")
    ref_stripped = ref.strip("[](){}")
    if pred_stripped == ref_stripped:
        return True

    # 深度感知地剥一层完整外层括号(避免误吃 \frac{...} 尾部的 })
    pred_inner = _peel_outer_bracket(pred)
    ref_inner = _peel_outer_bracket(ref)

    # 进制下标兼容:题面已指定基时,gt 常省略 _k 后缀
    # e.g. extracted="2003_6"  vs  gt="2003"  → 视为同一数
    # 但仅允许"一侧带后缀,另一侧裸数,前缀完全相同"。"_6" vs "_10" 仍不等。
    m_pred = re.match(r'^(.+)_\d+$', pred.strip())
    m_ref = re.match(r'^(.+)_\d+$', ref.strip())
    if m_pred and not m_ref and m_pred.group(1).strip() == ref.strip():
        return True
    if m_ref and not m_pred and m_ref.group(1).strip() == pred.strip():
        return True

    # 逗号分隔的复合答案(坐标 "(1,2)"、集合 "a,b,c"、向量等)
    if "," in pred_inner and "," in ref_inner:
        pred_parts = [p.strip() for p in pred_inner.split(",")]
        ref_parts = [p.strip() for p in ref_inner.split(",")]
        if len(pred_parts) == len(ref_parts):
            # 顺序敏感:坐标/向量
            if all(_math_equal(p, r, tolerance) for p, r in zip(pred_parts, ref_parts)):
                return True
            # 顺序无关:解集/标签集合 — 仅当两侧都不是 () 包裹的元组时启用
            # (pmatrix 已被 _pmatrix_to_tuple 转成 (a,b) 形式,会因外括号被排除)
            pred_is_tuple = pred.strip().startswith('(') and pred.strip().endswith(')')
            ref_is_tuple = ref.strip().startswith('(') and ref.strip().endswith(')')
            if not (pred_is_tuple or ref_is_tuple):
                matched = [False] * len(ref_parts)
                ok = True
                for p in pred_parts:
                    for j, r in enumerate(ref_parts):
                        if not matched[j] and _math_equal(p, r, tolerance):
                            matched[j] = True
                            break
                    else:
                        ok = False
                        break
                if ok:
                    return True

    return _symbolic_equal(prediction, reference, tolerance)


# ---------------------------------------------------------------------------
#  公开 API
# ---------------------------------------------------------------------------

def verify_math_answer(model_output: str, ground_truth: str) -> float:
    """验证模型数学答案是否正确（Reward 组件的核心调用）。

    流程：
      1. 从模型输出中提取答案（支持 \\boxed{}, ####, ### ANSWER: 格式）
      2. 第 1 层：Hendrycks 标准字符串归一化 + 精确匹配
      3. 第 2 层：SymPy 代数/数值等价性回退

    Args:
        model_output: 模型的完整输出文本。
        ground_truth: 标准答案字符串。

    Returns:
        float: 1.0 表示正确，0.0 表示错误。
    """
    extracted = extract_answer(model_output)
    ground_truth = str(ground_truth).strip()

    # 第 1 层：Hendrycks 字符串等价
    if is_equiv(extracted, ground_truth):
        return 1.0

    # 第 2 层：先做 Hendrycks 归一化，让 "k = ...", \tfrac/\dfrac, \frac{a}b 等差异
    # 在进入数值/符号判等前被消除,使 _try_parse_number 能命中数值短路。
    try:
        norm_pred = strip_string(extracted)
        norm_ref = strip_string(ground_truth)
    except Exception:
        norm_pred, norm_ref = extracted, ground_truth

    try:
        if _math_equal(norm_pred, norm_ref):
            return 1.0
    except Exception:
        pass

    return 0.0
