feat(ProcedureCodes updated)
This commit is contained in:
@@ -1,48 +1,105 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0120",
|
"Procedure Code": "D0120",
|
||||||
"Description": "perio exam",
|
"Description": "Periodic oral evaluation - established patient",
|
||||||
"Price": "105"
|
"PriceLTEQ21": "31",
|
||||||
|
"PriceGT21": "24"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0140",
|
"Procedure Code": "D0140",
|
||||||
"Description": "limited exam",
|
"Description": "Limited oral evaluation - problem focused",
|
||||||
"Price": "90"
|
"PriceLTEQ21": "49",
|
||||||
|
"PriceGT21": "43"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D0145",
|
||||||
|
"Description": "Oral evaluation for a patient under three years of age and counseling with primary caregiver",
|
||||||
|
"PriceLTEQ21": "27",
|
||||||
|
"PriceGT21": "NC"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0150",
|
"Procedure Code": "D0150",
|
||||||
"Description": "comprehensive exam",
|
"Description": "Comprehensive oral evaluation - new or established patient",
|
||||||
"Price": "120"
|
"PriceLTEQ21": "62",
|
||||||
|
"PriceGT21": "41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D0180",
|
||||||
|
"Description": "Comprehensive periodontal evaluation - new or established patient",
|
||||||
|
"PriceLTEQ21": "58",
|
||||||
|
"PriceGT21": "37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D0190",
|
||||||
|
"Description": "Screening of a patient (PHDH only)",
|
||||||
|
"PriceLTEQ21": "29",
|
||||||
|
"PriceGT21": "20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D0191",
|
||||||
|
"Description": "Assessment of a patient (PHDH only)",
|
||||||
|
"PriceLTEQ21": "29",
|
||||||
|
"PriceGT21": "20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0210",
|
"Procedure Code": "D0210",
|
||||||
"Description": "Fmx.",
|
"Description": "Intraoral - complete series of radiographic images",
|
||||||
"Price": "120"
|
"PriceLTEQ21": "94",
|
||||||
|
"PriceGT21": "76"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0220",
|
"Procedure Code": "D0220",
|
||||||
"Description": "first PA.",
|
"Description": "Intraoral - periapical, first radiographic image",
|
||||||
"Price": "60"
|
"PriceLTEQ21": "21",
|
||||||
|
"PriceGT21": "15"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0230",
|
"Procedure Code": "D0230",
|
||||||
"Description": "2nd PA.",
|
"Description": "Intraoral - periapical, each additional radiographic image",
|
||||||
"Price": "50"
|
"PriceLTEQ21": "17",
|
||||||
|
"PriceGT21": "13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D0240",
|
||||||
|
"Description": "Intraoral - occlusal radiographic image",
|
||||||
|
"PriceLTEQ21": "26",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D0270",
|
||||||
|
"Description": "Bitewing - single radiographic image",
|
||||||
|
"PriceLTEQ21": "17",
|
||||||
|
"PriceGT21": "14"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0272",
|
"Procedure Code": "D0272",
|
||||||
"Description": "2 BW",
|
"Description": "Bitewings - two radiographic images",
|
||||||
"Price": "80"
|
"PriceLTEQ21": "32",
|
||||||
|
"PriceGT21": "25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D0273",
|
||||||
|
"Description": "Bitewings - three radiographic images",
|
||||||
|
"PriceLTEQ21": "35",
|
||||||
|
"PriceGT21": "27"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0274",
|
"Procedure Code": "D0274",
|
||||||
"Description": "4BW",
|
"Description": "Bitewings - four radiographic images",
|
||||||
"Price": "160"
|
"PriceLTEQ21": "46",
|
||||||
|
"PriceGT21": "36"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0330",
|
"Procedure Code": "D0330",
|
||||||
"Description": "pano",
|
"Description": "Panoramic radiographic image",
|
||||||
"Price": "150"
|
"PriceLTEQ21": "94",
|
||||||
|
"PriceGT21": "69"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D0340",
|
||||||
|
"Description": "Cephalometric radiograph image (Oral surgeon only)",
|
||||||
|
"PriceLTEQ21": "85",
|
||||||
|
"PriceGT21": "74"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D0364",
|
"Procedure Code": "D0364",
|
||||||
@@ -91,23 +148,129 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D1110",
|
"Procedure Code": "D1110",
|
||||||
"Description": "adult prophy",
|
"Description": "Prophylaxis – adult, 14 yo or older",
|
||||||
"Price": "150"
|
"PriceLTEQ21": "75",
|
||||||
|
"PriceGT21": "60"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D1120",
|
"Procedure Code": "D1120",
|
||||||
"Description": "child prophy",
|
"Description": "Prophylaxis – child, 0-13 yo",
|
||||||
"Price": "120"
|
"PriceLTEQ21": "55",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1206",
|
||||||
|
"Description": "Topical application of fluoride varnish",
|
||||||
|
"PriceLTEQ21": "28",
|
||||||
|
"PriceGT21": "26"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D1208",
|
"Procedure Code": "D1208",
|
||||||
"Description": "FL",
|
"Description": "Topical application of fluoride – excluding varnish",
|
||||||
"Price": "90"
|
"PriceLTEQ21": "31",
|
||||||
|
"PriceGT21": "29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D1351",
|
"Procedure Code": "D1351",
|
||||||
"Description": "sealant",
|
"Description": "Sealant – per tooth",
|
||||||
"Price": "80"
|
"PriceLTEQ21": "44",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1354",
|
||||||
|
"Description": "Application of caries arresting medicament - per tooth",
|
||||||
|
"PriceLTEQ21": "15",
|
||||||
|
"PriceGT21": "15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1510",
|
||||||
|
"Description": "Space maintainer – fixed,unilateral – per quadrant",
|
||||||
|
"PriceLTEQ21": "229",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1516",
|
||||||
|
"Description": "Space maintainer- fixed- bilateral, maxillary",
|
||||||
|
"PriceLTEQ21": "345",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1517",
|
||||||
|
"Description": "Space maintainer- fixed- bilateral, mandibular",
|
||||||
|
"PriceLTEQ21": "345",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1520",
|
||||||
|
"Description": "Space maintainer – removable- unilateral- per quadrant",
|
||||||
|
"PriceLTEQ21": "244",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1526",
|
||||||
|
"Description": "Space maintainer- removable- bilateral, maxillary",
|
||||||
|
"PriceLTEQ21": "368",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1527",
|
||||||
|
"Description": "Space maintainer- removable- bilateral, mandibular",
|
||||||
|
"PriceLTEQ21": "368",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1575",
|
||||||
|
"Description": "Distal shoe space maintainer - fixed- unilateral- Per Quadrant I.C",
|
||||||
|
"PriceLTEQ21": "NC",
|
||||||
|
"PriceGT21": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1701",
|
||||||
|
"Description": "Pfizer-BioNTech Covid-19 vaccine administration – first dose SARSCOV2 COVID-19 VAC mRNA 30mcg/0.3mL IM DOSE 1",
|
||||||
|
"PriceLTEQ21": "45.87",
|
||||||
|
"PriceGT21": "45.87"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1702",
|
||||||
|
"Description": "Pfizer-BioNTech Covid-19 vaccine administration – second dose SARSCOV2 COVID-19 VAC mRNA 30mcg/0.3mL IM DOSE 2",
|
||||||
|
"PriceLTEQ21": "45.87",
|
||||||
|
"PriceGT21": "45.87"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1707",
|
||||||
|
"Description": "Janssen Covid-19 vaccine administration SARSCOV2 COVID-19 VAC Ad26 5x1010 VP/.5mL IM SINGLE DOSE",
|
||||||
|
"PriceLTEQ21": "45.87",
|
||||||
|
"PriceGT21": "45.87"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1708",
|
||||||
|
"Description": "Pfizer-BioNTech Covid-19 vaccine administration – third dose",
|
||||||
|
"PriceLTEQ21": "45.87",
|
||||||
|
"PriceGT21": "45.87"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1709",
|
||||||
|
"Description": "Pfizer-BioNTech Covid-19 vaccine administration – booster dose",
|
||||||
|
"PriceLTEQ21": "45.87",
|
||||||
|
"PriceGT21": "45.87"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1712",
|
||||||
|
"Description": "Janssen Covid-19 vaccine administration - booster dose",
|
||||||
|
"PriceLTEQ21": "45.87",
|
||||||
|
"PriceGT21": "45.87"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1713",
|
||||||
|
"Description": "Pfizer-BioNTech Covid-19 vaccine administration tris-sucrose pediatric – first dose",
|
||||||
|
"PriceLTEQ21": "45.87",
|
||||||
|
"PriceGT21": "45.87"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Procedure Code": "D1714",
|
||||||
|
"Description": "Pfizer-BioNTech Covid-19 vaccine administration tris-sucrose pediatric – second dose",
|
||||||
|
"PriceLTEQ21": "45.87",
|
||||||
|
"PriceGT21": "45.87"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D1999",
|
"Procedure Code": "D1999",
|
||||||
@@ -116,13 +279,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D2140",
|
"Procedure Code": "D2140",
|
||||||
"Description": "amalgam, one surface",
|
"Description": "Amalgam-one surface, primary or permanent",
|
||||||
"Price": "150"
|
"PriceLTEQ21": "77",
|
||||||
|
"PriceGT21": "62"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D2150",
|
"Procedure Code": "D2150",
|
||||||
"Description": "amalgam, two surface",
|
"Description": "Amalgam-two surfaces, primary or permanent",
|
||||||
"Price": "200"
|
"PriceLTEQ21": "95",
|
||||||
|
"PriceGT21": "77"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D2955",
|
"Procedure Code": "D2955",
|
||||||
@@ -930,8 +1095,8 @@
|
|||||||
{
|
{
|
||||||
"Procedure Code": "D8999",
|
"Procedure Code": "D8999",
|
||||||
"Description": "Unspecified orthodontic procedure, by report (Orthodontist only) I.C I.C** Y Y**",
|
"Description": "Unspecified orthodontic procedure, by report (Orthodontist only) I.C I.C** Y Y**",
|
||||||
"PriceLTEQ21": "IC",
|
"PriceLTEQ21": "NC",
|
||||||
"PriceGT21": "IC"
|
"PriceGT21": "NC"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Procedure Code": "D9110",
|
"Procedure Code": "D9110",
|
||||||
|
|||||||
BIN
apps/ProcedureCodeFromMhPdf/MHv2.pdf
Normal file
BIN
apps/ProcedureCodeFromMhPdf/MHv2.pdf
Normal file
Binary file not shown.
@@ -17,10 +17,10 @@ from typing import List, Dict, Any
|
|||||||
# =========================
|
# =========================
|
||||||
# CONFIG — EDIT THESE ONLY
|
# CONFIG — EDIT THESE ONLY
|
||||||
# =========================
|
# =========================
|
||||||
MAIN_PATH = "procedureCodesMain.json" # your main JSON (with PriceLTEQ21/PriceGT21)
|
MAIN_PATH = "procedureCodes_v2.json" # your main JSON (with PriceLTEQ21/PriceGT21)
|
||||||
OTHER_PATHS = [
|
OTHER_PATHS = [
|
||||||
"procedureCodesOld.json", # one or more other JSON files to compare against the main
|
# "procedureCodesOld.json", # one or more other JSON files to compare against the main
|
||||||
# "other2.json",
|
"output.json",
|
||||||
]
|
]
|
||||||
OUT_PATH = "not_in_main.json" # where to write the results
|
OUT_PATH = "not_in_main.json" # where to write the results
|
||||||
# =========================
|
# =========================
|
||||||
|
|||||||
241
apps/ProcedureCodeFromMhPdf/compareJson_matchingPrice.py
Normal file
241
apps/ProcedureCodeFromMhPdf/compareJson_matchingPrice.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Compare prices between two JSON files (file1 vs file2) — CONFIG-DRIVEN version.
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Loads two JSON arrays of records (file1 and file2).
|
||||||
|
- Indexes by procedure code (tries common keys like "Procedure Code", "Code", etc).
|
||||||
|
- Normalizes money tokens: removes $ and commas, treats "NC" as literal.
|
||||||
|
- Compares all three price fields:
|
||||||
|
- Price
|
||||||
|
- PriceLTEQ21
|
||||||
|
- PriceGT21
|
||||||
|
Matching rules:
|
||||||
|
- If both records have the same named field, compare them.
|
||||||
|
- If file1 has only a single "Price" and file2 has PriceLTEQ21 / PriceGT21,
|
||||||
|
the script will compare file1.Price to BOTH PriceLTEQ21 and PriceGT21 (and
|
||||||
|
report mismatch if file1.Price differs from either).
|
||||||
|
- "NC" only equals "NC".
|
||||||
|
- Numeric tokens compared numerically within tolerance (default 0.005).
|
||||||
|
- Produces output JSON (configured below) listing:
|
||||||
|
- mismatches: detailed entries for codes that differ
|
||||||
|
- only_in_file1: codes found only in file1
|
||||||
|
- only_in_file2: codes found only in file2
|
||||||
|
- summary
|
||||||
|
|
||||||
|
Edit the CONFIG block below, then run the script.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# CONFIG — EDIT THESE ONLY
|
||||||
|
# =========================
|
||||||
|
FILE1_PATH = "procedureCodes_v2.json" # path to file 1 (your base/reference file)
|
||||||
|
FILE2_PATH = "output.json" # path to file 2 (the file to compare)
|
||||||
|
OUT_PATH = "price_diffs.json" # output JSON writing mismatches
|
||||||
|
TOLERANCE = 0.005 # numeric tolerance for floats
|
||||||
|
CODE_KEY_CANDIDATES = ("Procedure Code", "Code", "procedure_code", "procedure code")
|
||||||
|
# If True: when file1 has single "Price" and file2 has both LTEQ/GT values,
|
||||||
|
# compare file1.Price against both fields and flag mismatch if either differs.
|
||||||
|
COMPARE_SINGLE_PRICE_AGAINST_BOTH = True
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
_money_re = re.compile(r"^\s*(NC|\$?\s*[\d,]+(?:\.\d{1,2})?)\s*$", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_money_token(token: Optional[str]) -> Optional[str]:
|
||||||
|
"""Normalize money token to canonical string or 'NC'. Return None if missing/empty."""
|
||||||
|
if token is None:
|
||||||
|
return None
|
||||||
|
t = str(token).strip()
|
||||||
|
if not t:
|
||||||
|
return None
|
||||||
|
m = _money_re.match(t)
|
||||||
|
if not m:
|
||||||
|
# unknown format — return trimmed token so mismatch is visible
|
||||||
|
return t
|
||||||
|
val = m.group(1)
|
||||||
|
if val.upper() == "NC":
|
||||||
|
return "NC"
|
||||||
|
val = val.replace("$", "").replace(",", "").strip()
|
||||||
|
# Remove trailing zeros from decimals, but preserve integer form
|
||||||
|
if "." in val:
|
||||||
|
val = val.rstrip("0").rstrip(".")
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def numeric_compare(a: Optional[str], b: Optional[str], tol: float = TOLERANCE) -> bool:
|
||||||
|
"""Compare normalized tokens. NC compares only equal to NC. Otherwise numeric compare."""
|
||||||
|
if a is None or b is None:
|
||||||
|
return False
|
||||||
|
if a == b:
|
||||||
|
return True
|
||||||
|
if a.upper() == "NC" or b.upper() == "NC":
|
||||||
|
return a.upper() == b.upper()
|
||||||
|
try:
|
||||||
|
return abs(float(a) - float(b)) <= tol
|
||||||
|
except Exception:
|
||||||
|
# fallback to exact match if non-numeric
|
||||||
|
return a == b
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: str) -> List[Dict[str, Any]]:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
raise ValueError(f"Expected JSON array in {path}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def build_index(records: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Index records by procedure code. First match wins for duplicates."""
|
||||||
|
idx: Dict[str, Dict[str, Any]] = {}
|
||||||
|
for rec in records:
|
||||||
|
code = None
|
||||||
|
for k in CODE_KEY_CANDIDATES:
|
||||||
|
if k in rec and rec[k]:
|
||||||
|
code = str(rec[k]).strip()
|
||||||
|
break
|
||||||
|
if not code:
|
||||||
|
# try to find any field with a Dxxxx-like value
|
||||||
|
for v in rec.values():
|
||||||
|
if isinstance(v, str) and re.match(r"^\s*D\d{4}\s*$", v):
|
||||||
|
code = v.strip()
|
||||||
|
break
|
||||||
|
if not code:
|
||||||
|
continue
|
||||||
|
if code in idx:
|
||||||
|
# duplicate: keep first occurrence
|
||||||
|
continue
|
||||||
|
idx[code] = rec
|
||||||
|
return idx
|
||||||
|
|
||||||
|
|
||||||
|
def extract_price_fields(rec: Dict[str, Any]) -> Dict[str, Optional[str]]:
|
||||||
|
"""
|
||||||
|
Return dict with normalized values for 'Price', 'PriceLTEQ21', and 'PriceGT21'.
|
||||||
|
Keys always present with None when missing.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"Price": normalize_money_token(rec.get("Price")),
|
||||||
|
"PriceLTEQ21": normalize_money_token(rec.get("PriceLTEQ21")),
|
||||||
|
"PriceGT21": normalize_money_token(rec.get("PriceGT21")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compare_code_records(code: str, rec1: Dict[str, Any], rec2: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Compare price fields for a single code. Return mismatch dict if any mismatch present, else None.
|
||||||
|
Mismatch dict includes file1/file2 price fields and per-field mismatch details.
|
||||||
|
"""
|
||||||
|
p1 = extract_price_fields(rec1)
|
||||||
|
p2 = extract_price_fields(rec2)
|
||||||
|
|
||||||
|
mismatches = []
|
||||||
|
|
||||||
|
# 1) Compare same-named fields if both present
|
||||||
|
for key in ("Price", "PriceLTEQ21", "PriceGT21"):
|
||||||
|
a = p1.get(key)
|
||||||
|
b = p2.get(key)
|
||||||
|
if a is None and b is None:
|
||||||
|
continue
|
||||||
|
if a is None or b is None:
|
||||||
|
# present in one but not the other: count as mismatch
|
||||||
|
mismatches.append({"field": key, "file1": a, "file2": b, "reason": "missing_in_one"})
|
||||||
|
continue
|
||||||
|
if not numeric_compare(a, b):
|
||||||
|
mismatches.append({"field": key, "file1": a, "file2": b, "reason": "value_mismatch"})
|
||||||
|
|
||||||
|
# 2) Special-case: if file1 has only single Price, and file2 has LTEQ/GT present,
|
||||||
|
# optionally compare file1.Price against each of them.
|
||||||
|
if COMPARE_SINGLE_PRICE_AGAINST_BOTH:
|
||||||
|
# Only apply if file1.Price exists and file1 does NOT have LTEQ/GT (both None),
|
||||||
|
# but file2 has at least one of LTEQ/GT.
|
||||||
|
file1_has_price = p1.get("Price") is not None
|
||||||
|
file1_has_any_special = (p1.get("PriceLTEQ21") is not None) or (p1.get("PriceGT21") is not None)
|
||||||
|
file2_has_any_special = (p2.get("PriceLTEQ21") is not None) or (p2.get("PriceGT21") is not None)
|
||||||
|
if file1_has_price and (not file1_has_any_special) and file2_has_any_special:
|
||||||
|
# compare file1.Price to each present file2 special price
|
||||||
|
left = p1.get("Price")
|
||||||
|
for special_key in ("PriceLTEQ21", "PriceGT21"):
|
||||||
|
right = p2.get(special_key)
|
||||||
|
if right is None:
|
||||||
|
continue
|
||||||
|
# If already recorded a same-named mismatch for this special_key above,
|
||||||
|
# that mismatch covered the case where file1 was missing that named field.
|
||||||
|
# But since file1 lacked that special field, we still want to compare single Price vs special.
|
||||||
|
if not numeric_compare(left, right):
|
||||||
|
mismatches.append({
|
||||||
|
"field": f"Price_vs_{special_key}",
|
||||||
|
"file1": left,
|
||||||
|
"file2": right,
|
||||||
|
"reason": "single_price_vs_special_mismatch"
|
||||||
|
})
|
||||||
|
|
||||||
|
if mismatches:
|
||||||
|
return {
|
||||||
|
"Procedure Code": code,
|
||||||
|
"Description_file1": rec1.get("Description"),
|
||||||
|
"Description_file2": rec2.get("Description"),
|
||||||
|
"file1_prices": p1,
|
||||||
|
"file2_prices": p2,
|
||||||
|
"mismatches": mismatches
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# load inputs
|
||||||
|
data1 = load_json(FILE1_PATH)
|
||||||
|
data2 = load_json(FILE2_PATH)
|
||||||
|
|
||||||
|
idx1 = build_index(data1)
|
||||||
|
idx2 = build_index(data2)
|
||||||
|
|
||||||
|
codes_all = sorted(set(list(idx1.keys()) + list(idx2.keys())))
|
||||||
|
|
||||||
|
mismatched: List[Dict[str, Any]] = []
|
||||||
|
only_in_file1: List[str] = []
|
||||||
|
only_in_file2: List[str] = []
|
||||||
|
|
||||||
|
for code in codes_all:
|
||||||
|
rec1 = idx1.get(code)
|
||||||
|
rec2 = idx2.get(code)
|
||||||
|
if rec1 is None:
|
||||||
|
only_in_file2.append(code)
|
||||||
|
continue
|
||||||
|
if rec2 is None:
|
||||||
|
only_in_file1.append(code)
|
||||||
|
continue
|
||||||
|
diff = compare_code_records(code, rec1, rec2)
|
||||||
|
if diff:
|
||||||
|
mismatched.append(diff)
|
||||||
|
|
||||||
|
out = {
|
||||||
|
"summary": {
|
||||||
|
"total_codes_found": len(codes_all),
|
||||||
|
"only_in_file1_count": len(only_in_file1),
|
||||||
|
"only_in_file2_count": len(only_in_file2),
|
||||||
|
"mismatched_count": len(mismatched),
|
||||||
|
},
|
||||||
|
"only_in_file1": only_in_file1,
|
||||||
|
"only_in_file2": only_in_file2,
|
||||||
|
"mismatches": mismatched
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(OUT_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(out, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# brief console summary
|
||||||
|
print(f"Compared {len(codes_all)} procedure codes.")
|
||||||
|
print(f"Only in {FILE1_PATH}: {len(only_in_file1)} codes.")
|
||||||
|
print(f"Only in {FILE2_PATH}: {len(only_in_file2)} codes.")
|
||||||
|
print(f"Mismatched prices: {len(mismatched)} codes.")
|
||||||
|
print(f"Wrote detailed diffs to {OUT_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -31,9 +31,9 @@ import fitz # PyMuPDF
|
|||||||
# =========================
|
# =========================
|
||||||
# CONFIG — EDIT THESE ONLY
|
# CONFIG — EDIT THESE ONLY
|
||||||
# =========================
|
# =========================
|
||||||
PDF_PATH = "MH.pdf" # path to your PDF
|
PDF_PATH = "MHv2.pdf" # path to your PDF
|
||||||
PAGE_START = 1 # 1-based inclusive start page (e.g., 1)
|
PAGE_START = 1 # 1-based inclusive start page (e.g., 1)
|
||||||
PAGE_END = 12 # 1-based inclusive end page (e.g., 5)
|
PAGE_END = 15 # 1-based inclusive end page (e.g., 5)
|
||||||
OUT_PATH = "output.json" # single JSON file containing all parsed rows
|
OUT_PATH = "output.json" # single JSON file containing all parsed rows
|
||||||
FIRST_PRICE_IS_LTE21 = True # True => first price line is <=21; False => first price is >21
|
FIRST_PRICE_IS_LTE21 = True # True => first price line is <=21; False => first price is >21
|
||||||
PRINT_PAGE_TEXT = False # set True to print raw text for each page
|
PRINT_PAGE_TEXT = False # set True to print raw text for each page
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0120",
|
|
||||||
"Description": "perio exam",
|
|
||||||
"Price": "105"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0140",
|
|
||||||
"Description": "limited exam",
|
|
||||||
"Price": "90"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0150",
|
|
||||||
"Description": "comprehensive exam",
|
|
||||||
"Price": "120"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0210",
|
|
||||||
"Description": "Fmx.",
|
|
||||||
"Price": "120"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0220",
|
|
||||||
"Description": "first PA.",
|
|
||||||
"Price": "60"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0230",
|
|
||||||
"Description": "2nd PA.",
|
|
||||||
"Price": "50"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0272",
|
|
||||||
"Description": "2 BW",
|
|
||||||
"Price": "80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0274",
|
|
||||||
"Description": "4BW",
|
|
||||||
"Price": "160"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0330",
|
|
||||||
"Description": "pano",
|
|
||||||
"Price": "150"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0364",
|
|
||||||
"Description": "Less than one jaw",
|
|
||||||
"Price": "350"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0365",
|
|
||||||
"Description": "Mand",
|
|
||||||
"Price": "350"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0366",
|
|
||||||
"Description": "Max",
|
|
||||||
"Price": "350"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0367",
|
|
||||||
"Description": "",
|
|
||||||
"Price": "400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0368",
|
|
||||||
"Description": "include TMJ",
|
|
||||||
"Price": "375"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0380",
|
|
||||||
"Description": "Less than one jaw",
|
|
||||||
"Price": "300"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0381",
|
|
||||||
"Description": "Mand",
|
|
||||||
"Price": "300"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0382",
|
|
||||||
"Description": "Max",
|
|
||||||
"Price": "300"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D0383",
|
|
||||||
"Description": "",
|
|
||||||
"Price": "350"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D1110",
|
|
||||||
"Description": "adult prophy",
|
|
||||||
"Price": "150"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D1120",
|
|
||||||
"Description": "child prophy",
|
|
||||||
"Price": "120"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D1208",
|
|
||||||
"Description": "FL",
|
|
||||||
"Price": "90"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D1351",
|
|
||||||
"Description": "sealant",
|
|
||||||
"Price": "80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D1999",
|
|
||||||
"Description": "",
|
|
||||||
"Price": "50"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D2140",
|
|
||||||
"Description": "amalgam, one surface",
|
|
||||||
"Price": "150"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D2150",
|
|
||||||
"Description": "amalgam, two surface",
|
|
||||||
"Price": "200"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D2955",
|
|
||||||
"Description": "post renoval",
|
|
||||||
"Price": "350"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D4910",
|
|
||||||
"Description": "perio maintains",
|
|
||||||
"Price": "250"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D5510",
|
|
||||||
"Description": "Repair broken complete denture base (QUAD)",
|
|
||||||
"Price": "400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D6056",
|
|
||||||
"Description": "pre fab abut",
|
|
||||||
"Price": "750"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D6057",
|
|
||||||
"Description": "custom abut",
|
|
||||||
"Price": "800"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D6058",
|
|
||||||
"Description": "porcelain, implant crown, ceramic crown",
|
|
||||||
"Price": "1400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D6059",
|
|
||||||
"Description": "",
|
|
||||||
"Price": "1400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D6100",
|
|
||||||
"Description": "",
|
|
||||||
"Price": "320"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D6110",
|
|
||||||
"Description": "implant",
|
|
||||||
"Price": "1600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D6242",
|
|
||||||
"Description": "noble metal. For united",
|
|
||||||
"Price": "1400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D6245",
|
|
||||||
"Description": "porcelain, not for united",
|
|
||||||
"Price": "1400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D7910",
|
|
||||||
"Description": "suture, small wound up to 5 mm",
|
|
||||||
"Price": "400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Procedure Code": "D7950",
|
|
||||||
"Description": "max",
|
|
||||||
"Price": "800"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
1004
apps/ProcedureCodeFromMhPdf/output.json
Normal file
1004
apps/ProcedureCodeFromMhPdf/output.json
Normal file
File diff suppressed because it is too large
Load Diff
1192
apps/ProcedureCodeFromMhPdf/procedureCodes_v2.json
Normal file
1192
apps/ProcedureCodeFromMhPdf/procedureCodes_v2.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user