diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 24e1bbe33..3cd329a56 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -634,6 +634,14 @@ const TRANSLATIONS: Partial>> = { "leftover-map axis 2": "잔여 지도 축 2", "leftover-map axis {axis} ({share}%)": "잔여 지도 축 {axis} ({share}%)", "leftover-map axis {axis} tick {value}": "잔여 지도 축 {axis} 눈금 {value}", + "leftover map comparison graphic leftover-map axis {axis} tick {value}": "잔여 지도 비교 그래픽 잔여 지도 축 {axis} 눈금 {value}", + "leftover map comparison graphic leftover-map axis {axis} tick {value} {share}%": "잔여 지도 비교 그래픽 잔여 지도 축 {axis} 눈금 {value} {share}%", + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}": "잔여 지도 비교 그래픽 잔여 지도 축 {axis} 눈금 {value} σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular} {share}%": "잔여 지도 비교 그래픽 잔여 지도 축 {axis} 눈금 {value} σ {singular} {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value}": "잔여 지도 비교 그래픽 잔여 지도 축 {axis} 원점 눈금 {value}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} {share}%": "잔여 지도 비교 그래픽 잔여 지도 축 {axis} 원점 눈금 {value} {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular}": "잔여 지도 비교 그래픽 잔여 지도 축 {axis} 원점 눈금 {value} σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular} {share}%": "잔여 지도 비교 그래픽 잔여 지도 축 {axis} 원점 눈금 {value} σ {singular} {share}%", "leftover-map distance {label}": "잔여 지도 거리 {label}", "leftover-map reconstruction {label}": "잔여 지도 재구성 {label}", "leftover-map explained leftover share {label}": "잔여 지도 설명 잔여 점유율 {label}", @@ -1270,6 +1278,14 @@ const TRANSLATIONS: Partial>> = { "leftover-map axis 2": "残差图轴 2", "leftover-map axis {axis} ({share}%)": "残差图轴 {axis} ({share}%)", "leftover-map axis {axis} tick {value}": "残差图轴 {axis} 刻度 {value}", + "leftover map comparison graphic leftover-map axis {axis} tick {value}": "残差图比较图形 残差图轴 {axis} 刻度 {value}", + "leftover map comparison graphic leftover-map axis {axis} tick {value} {share}%": "残差图比较图形 残差图轴 {axis} 刻度 {value} {share}%", + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}": "残差图比较图形 残差图轴 {axis} 刻度 {value} σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular} {share}%": "残差图比较图形 残差图轴 {axis} 刻度 {value} σ {singular} {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value}": "残差图比较图形 残差图轴 {axis} 原点刻度 {value}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} {share}%": "残差图比较图形 残差图轴 {axis} 原点刻度 {value} {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular}": "残差图比较图形 残差图轴 {axis} 原点刻度 {value} σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular} {share}%": "残差图比较图形 残差图轴 {axis} 原点刻度 {value} σ {singular} {share}%", "leftover-map distance {label}": "残差图距离 {label}", "leftover-map reconstruction {label}": "残差图重建 {label}", "leftover-map explained leftover share {label}": "残差图已解释残差份额 {label}", @@ -1910,6 +1926,14 @@ const TRANSLATIONS: Partial>> = { "leftover-map axis 2": "残差マップ軸 2", "leftover-map axis {axis} ({share}%)": "残差マップ軸 {axis} ({share}%)", "leftover-map axis {axis} tick {value}": "残差マップ軸 {axis} 目盛 {value}", + "leftover map comparison graphic leftover-map axis {axis} tick {value}": "残差マップ比較グラフィック 残差マップ軸 {axis} 目盛 {value}", + "leftover map comparison graphic leftover-map axis {axis} tick {value} {share}%": "残差マップ比較グラフィック 残差マップ軸 {axis} 目盛 {value} {share}%", + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}": "残差マップ比較グラフィック 残差マップ軸 {axis} 目盛 {value} σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular} {share}%": "残差マップ比較グラフィック 残差マップ軸 {axis} 目盛 {value} σ {singular} {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value}": "残差マップ比較グラフィック 残差マップ軸 {axis} 原点目盛 {value}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} {share}%": "残差マップ比較グラフィック 残差マップ軸 {axis} 原点目盛 {value} {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular}": "残差マップ比較グラフィック 残差マップ軸 {axis} 原点目盛 {value} σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular} {share}%": "残差マップ比較グラフィック 残差マップ軸 {axis} 原点目盛 {value} σ {singular} {share}%", "leftover-map distance {label}": "残差マップ距離 {label}", "leftover-map reconstruction {label}": "残差マップ再構成 {label}", "leftover-map explained leftover share {label}": "残差マップ説明済み残差割合 {label}", @@ -2550,6 +2574,14 @@ const TRANSLATIONS: Partial>> = { "leftover-map axis 2": "trục bản đồ phần dư 2", "leftover-map axis {axis} ({share}%)": "trục bản đồ phần dư {axis} ({share}%)", "leftover-map axis {axis} tick {value}": "vạch trục bản đồ phần dư {axis} {value}", + "leftover map comparison graphic leftover-map axis {axis} tick {value}": "đồ họa so sánh bản đồ phần dư, trục {axis}, vạch {value}", + "leftover map comparison graphic leftover-map axis {axis} tick {value} {share}%": "đồ họa so sánh bản đồ phần dư, trục {axis}, vạch {value}, {share}%", + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}": "đồ họa so sánh bản đồ phần dư, trục {axis}, vạch {value}, σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular} {share}%": "đồ họa so sánh bản đồ phần dư, trục {axis}, vạch {value}, σ {singular}, {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value}": "đồ họa so sánh bản đồ phần dư, trục {axis}, vạch gốc {value}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} {share}%": "đồ họa so sánh bản đồ phần dư, trục {axis}, vạch gốc {value}, {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular}": "đồ họa so sánh bản đồ phần dư, trục {axis}, vạch gốc {value}, σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular} {share}%": "đồ họa so sánh bản đồ phần dư, trục {axis}, vạch gốc {value}, σ {singular}, {share}%", "leftover-map distance {label}": "khoảng cách bản đồ phần dư {label}", "leftover-map reconstruction {label}": "tái dựng bản đồ phần dư {label}", "leftover-map explained leftover share {label}": "tỷ phần phần dư được giải thích trên bản đồ phần dư {label}", diff --git a/frontend/src/leftoverMapPlotAxisSingular.origin.test.ts b/frontend/src/leftoverMapPlotAxisSingular.origin.test.ts new file mode 100644 index 000000000..ce3df8362 --- /dev/null +++ b/frontend/src/leftoverMapPlotAxisSingular.origin.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { + LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK, + LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SHARE, + LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SINGULAR, + LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SINGULAR_SHARE, + LEFTOVER_MAP_COMPARE_PLOT_TICK, + leftoverMapComparePlotTickAxisBadge, + leftoverMapPlotTickIsOrigin, +} from "./leftoverMapPlotAxisSingular"; + +describe("comparison-graphic origin tick projection", () => { + it("recognizes only the canonical formatted zero coordinate as origin", () => { + expect(leftoverMapPlotTickIsOrigin("0.00")).toBe(true); + expect(leftoverMapPlotTickIsOrigin("+0.00")).toBe(false); + expect(leftoverMapPlotTickIsOrigin("−0.00")).toBe(false); + expect(leftoverMapPlotTickIsOrigin("0")).toBe(false); + }); + + it("keeps origin identity independent from persisted share and singular evidence", () => { + expect(leftoverMapComparePlotTickAxisBadge(1, "0.00", null, null).template).toBe( + LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK, + ); + expect(leftoverMapComparePlotTickAxisBadge(1, "0.00", null, 0.25).template).toBe( + LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SHARE, + ); + expect(leftoverMapComparePlotTickAxisBadge(1, "0.00", 1.5, null).template).toBe( + LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SINGULAR, + ); + expect(leftoverMapComparePlotTickAxisBadge(1, "0.00", 1.5, 0.25).template).toBe( + LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SINGULAR_SHARE, + ); + }); + + it("fails invalid evidence closed without turning a non-origin tick into origin", () => { + const badge = leftoverMapComparePlotTickAxisBadge( + 2, + "+0.50", + Number.NaN, + Number.POSITIVE_INFINITY, + ); + + expect(badge).toEqual({ + template: LEFTOVER_MAP_COMPARE_PLOT_TICK, + values: { axis: 2, value: "+0.50" }, + }); + }); +}); diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts index 7f7b81b02..7915eb9c3 100644 --- a/frontend/src/leftoverMapPlotAxisSingular.ts +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -5,6 +5,7 @@ import { formatLeftoverMapPlotAxisShare, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, } from "./leftoverMapPlotAxisShare"; +import { formatSignedLeftoverValue } from "./leftoverMapUnexplained"; export const LEFTOVER_MAP_PLOT_AXIS_SINGULAR = "leftover-map axis {axis} σ {value}"; @@ -36,15 +37,30 @@ export const LEFTOVER_MAP_PLOT_TICK_SHARE = export const LEFTOVER_MAP_PLOT_TICK_SINGULAR_SHARE = "leftover-map axis {axis} tick {value} σ {singular} {share}%"; -export const LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR = - "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}"; +export const LEFTOVER_MAP_COMPARE_PLOT_TICK = + "leftover map comparison graphic leftover-map axis {axis} tick {value}"; export const LEFTOVER_MAP_COMPARE_PLOT_TICK_SHARE = "leftover map comparison graphic leftover-map axis {axis} tick {value} {share}%"; +export const LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR = + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}"; + export const LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR_SHARE = "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular} {share}%"; +export const LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK = + "leftover map comparison graphic leftover-map axis {axis} origin tick {value}"; + +export const LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SHARE = + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} {share}%"; + +export const LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SINGULAR = + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular}"; + +export const LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SINGULAR_SHARE = + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular} {share}%"; + export const LEFTOVER_MAP_COMPARE_AXIS_TICK_SINGULAR = "leftover map comparison leftover axis {axis} tick {value} σ {singular}"; @@ -124,6 +140,54 @@ export function leftoverMapComparePlotAxisBadge( }; } +/** Exact origin identity follows the canonical persisted-coordinate formatter only. */ +export function leftoverMapPlotTickIsOrigin(tickLabel: string): boolean { + const originLabel = formatSignedLeftoverValue(0); + return originLabel !== null && tickLabel === originLabel; +} + +/** + * Project one comparison-graphic tick without deriving origin, share, or σ from + * each other. Each persisted evidence field can therefore fail closed on its own. + */ +export function leftoverMapComparePlotTickAxisBadge( + axisIndex: number, + tickLabel: string, + leftoverSingular: number | null | undefined, + leftoverShare?: number | null, +): LeftoverMapCompareAxisBadge { + const singular = formatLeftoverMapPlotAxisSingular(leftoverSingular); + const percent = formatLeftoverMapPlotAxisShare(leftoverShare); + const origin = leftoverMapPlotTickIsOrigin(tickLabel); + + if (singular === null && percent === null) { + return { + template: origin ? LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK : LEFTOVER_MAP_COMPARE_PLOT_TICK, + values: { axis: axisIndex, value: tickLabel }, + }; + } + if (singular === null) { + return { + template: origin ? LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SHARE : LEFTOVER_MAP_COMPARE_PLOT_TICK_SHARE, + values: { axis: axisIndex, value: tickLabel, share: percent as string }, + }; + } + if (percent === null) { + return { + template: origin + ? LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SINGULAR + : LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR, + values: { axis: axisIndex, value: tickLabel, singular }, + }; + } + return { + template: origin + ? LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK_SINGULAR_SHARE + : LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR_SHARE, + values: { axis: axisIndex, value: tickLabel, singular, share: percent }, + }; +} + /** * Compose comparison-strip axis evidence without deriving one persisted * measurement from the other. Missing or invalid evidence is omitted @@ -189,36 +253,6 @@ export function leftoverMapPlotTickAxisBadge( }; } -/** Compose persisted comparison-graphic tick σ/share without deriving either field. */ -export function leftoverMapComparePlotTickAxisBadge( - axisIndex: number, - tickLabel: string, - leftoverSingular: number | null | undefined, - leftoverShare?: LeftoverMapAxis["leftover_share"] | null, -): LeftoverMapCompareAxisBadge | null { - const singular = formatLeftoverMapPlotAxisSingular(leftoverSingular); - const share = formatLeftoverMapPlotAxisShare(leftoverShare); - if (singular === null && share === null) { - return null; - } - if (singular === null) { - return { - template: LEFTOVER_MAP_COMPARE_PLOT_TICK_SHARE, - values: { axis: axisIndex, value: tickLabel, share: share as string }, - }; - } - if (share === null) { - return { - template: LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR, - values: { axis: axisIndex, value: tickLabel, singular }, - }; - } - return { - template: LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR_SHARE, - values: { axis: axisIndex, value: tickLabel, singular, share }, - }; -} - /** Compose persisted comparison-strip tick σ/share without deriving either field. */ export function leftoverMapCompareAxisTickBadge( axisIndex: number, diff --git a/tests/test_grouping_comparison_graphic_origin_tick_contract.py b/tests/test_grouping_comparison_graphic_origin_tick_contract.py new file mode 100644 index 000000000..6307fbde8 --- /dev/null +++ b/tests/test_grouping_comparison_graphic_origin_tick_contract.py @@ -0,0 +1,41 @@ +"""Executable contract for comparison-graphic origin tick captions.""" + +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +SINGULAR_SOURCE = ROOT / "frontend" / "src" / "leftoverMapPlotAxisSingular.ts" + + +def test_comparison_graphic_origin_tick_has_distinct_four_state_copy() -> None: + """Origin ticks stay distinct across empty, share-only, singular-only, and combined evidence.""" + source = SINGULAR_SOURCE.read_text(encoding="utf-8") + + for text in ( + "leftover map comparison graphic leftover-map axis {axis} origin tick {value}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} {share}%", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular}", + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular} {share}%", + ): + assert text in source + assert "leftoverMapPlotTickIsOrigin" in source + assert "leftoverMapComparePlotTickAxisBadge" in source + + +def test_origin_is_exact_formatted_zero_not_derived_from_share_or_singular() -> None: + """Origin identity follows the canonical formatted zero tick and does not use evidence magnitude.""" + source = SINGULAR_SOURCE.read_text(encoding="utf-8") + helper = re.search( + r"export function leftoverMapPlotTickIsOrigin\(.*?\n}\n", + source, + re.DOTALL, + ) + assert helper is not None + helper_source = helper.group(0) + + assert "formatSignedLeftoverValue(0)" in helper_source + assert "tickLabel === originLabel" in helper_source + assert "leftover_share" not in helper_source + assert "singular" not in helper_source.lower() + assert "Math.abs" not in helper_source diff --git a/tests/test_grouping_comparison_graphic_tick_contract.py b/tests/test_grouping_comparison_graphic_tick_contract.py index d6614a610..c4ea8a8be 100644 --- a/tests/test_grouping_comparison_graphic_tick_contract.py +++ b/tests/test_grouping_comparison_graphic_tick_contract.py @@ -7,29 +7,25 @@ ROOT = Path(__file__).resolve().parents[1] PLOT_SOURCE = ROOT / "frontend" / "src" / "components" / "LeftoverMapPlot.tsx" LAYOUT_SOURCE = ROOT / "frontend" / "src" / "leftoverMapPlotLayout.ts" +SINGULAR_SOURCE = ROOT / "frontend" / "src" / "leftoverMapPlotAxisSingular.ts" -def test_comparison_graphic_names_ticks_with_distinct_accessible_copy() -> None: - """Comparison ticks keep localized comparison copy while report ticks may add persisted σ evidence.""" +def test_comparison_graphic_names_ticks_through_evidence_aware_badge_contract() -> None: + """Comparison ticks consume persisted σ/share evidence and preserve distinct origin copy.""" plot_source = PLOT_SOURCE.read_text(encoding="utf-8") + singular_source = SINGULAR_SOURCE.read_text(encoding="utf-8") - # v2.84 adds a report-only evidence badge. Comparison ticks must keep the - # existing localized comparison + generic tick composition rather than - # inheriting report evidence or introducing a comparison-only translation. - assert "leftoverMapPlotTickAxisBadge" in plot_source + assert "leftoverMapComparePlotTickAxisBadge" in plot_source assert "leftoverSingularForAxis(leftoverMapAxes, tick.axis)" in plot_source - assert re.search( - r'reportTickBadge\s*=\s*variant\s*===\s*"report"\s*\?\s*leftoverMapPlotTickAxisBadge\(', - plot_source, - re.DOTALL, - ) - assert re.search( - r'variant\s*===\s*"comparison"\s*\?\s*`\$\{t\(LEFTOVER_MAP_COMPARE_PLOT_LABEL\)\}:\s*\$\{tf\(LEFTOVER_MAP_PLOT_TICK,\s*\{\s*axis:\s*tick\.axis,\s*value:\s*tick\.label,?\s*\}\)\}`', - plot_source, - re.DOTALL, + assert "leftoverShareForAxis(leftoverMapAxes, tick.axis)" in plot_source + assert "tf(comparisonTickBadge.template, comparisonTickBadge.values)" in plot_source + assert ( + 'export const LEFTOVER_MAP_COMPARE_PLOT_TICK =\n' + ' "leftover map comparison graphic leftover-map axis {axis} tick {value}";' + in singular_source ) - assert "tf(reportTickBadge.template, reportTickBadge.values)" in plot_source - assert "LEFTOVER_MAP_COMPARE_PLOT_TICK" not in plot_source + assert "LEFTOVER_MAP_COMPARE_PLOT_ORIGIN_TICK" in singular_source + assert "LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR_SHARE" in singular_source def test_tick_positions_come_from_persisted_coordinates_not_distance() -> None: diff --git a/tests/test_grouping_comparison_graphic_tick_i18n_contract.py b/tests/test_grouping_comparison_graphic_tick_i18n_contract.py new file mode 100644 index 000000000..5b317d4d7 --- /dev/null +++ b/tests/test_grouping_comparison_graphic_tick_i18n_contract.py @@ -0,0 +1,36 @@ +"""Executable translation contract for comparison-graphic tick accessibility copy.""" + +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +I18N_SOURCE = ROOT / "frontend" / "src" / "i18n.ts" + +TEMPLATES = { + "leftover map comparison graphic leftover-map axis {axis} tick {value}": ("axis", "value"), + "leftover map comparison graphic leftover-map axis {axis} tick {value} {share}%": ("axis", "value", "share"), + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}": ("axis", "value", "singular"), + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular} {share}%": ("axis", "value", "singular", "share"), + "leftover map comparison graphic leftover-map axis {axis} origin tick {value}": ("axis", "value"), + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} {share}%": ("axis", "value", "share"), + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular}": ("axis", "value", "singular"), + "leftover map comparison graphic leftover-map axis {axis} origin tick {value} σ {singular} {share}%": ("axis", "value", "singular", "share"), +} + + +def test_comparison_tick_accessibility_templates_are_translated_for_each_catalog_locale() -> None: + """Every non-English catalog must translate every tick template without dropping placeholders.""" + source = I18N_SOURCE.read_text(encoding="utf-8") + + for template, placeholders in TEMPLATES.items(): + entries = re.findall( + rf'{re.escape(chr(34) + template + chr(34))}\s*:\s*"([^"]*)"', + source, + ) + assert len(entries) == 4, f"expected ko/zh/ja/vi translations for {template!r}" + for translated in entries: + for placeholder in placeholders: + assert f"{{{placeholder}}}" in translated, ( + f"translation for {template!r} dropped {{{placeholder}}}: {translated!r}" + )