[{"data":1,"prerenderedAt":2126},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period":2092,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period":2101,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period":2106,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period":2107},{"id":4,"title":5,"body":6,"dateModified":1560,"datePublished":1560,"description":2059,"extension":2060,"faq":2061,"keyword":2070,"kind":2071,"meta":2072,"navigation":96,"path":2081,"seo":2082,"slug":2085,"stem":2086,"topics":2087,"type":2090,"__hash__":2091},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period\u002Findex.md","Validate Dates Fall Within an Expected Period",{"type":7,"value":8,"toc":2047},"minimark",[9,24,29,61,64,68,478,489,634,638,1020,1039,1043,1392,1406,1660,1664,1675,1733,1736,1740,1914,1922,1926,1963,1967,1970,1974,1991,2000,2006,2012,2016,2043],[10,11,12,13,17,18,23],"p",{},"A monthly import — September's expenses, timesheets or sales — should contain September dates. In practice a few rows always do not: a receipt dated 2062 because someone typed the year backwards, ",[14,15,16],"em",{},"03\u002F09\u002F2026"," entered by a colleague who writes month first, a date that arrived as the number 46280 because a column was formatted as text, a line from August that should have been in last month's file, a date in October because the export ran on the 2nd. Loaded as they are, those rows land in the wrong period or vanish from date-filtered reports. Validating the dates before loading catches them, and most of them can not only be flagged but explained. This page builds a date validator in pandas that tags each problem row with a reason — out of period, future, implausible year, probable day\u002Fmonth swap, Excel serial number, not a date, non-working day — and suggests a corrected date where the evidence supports one. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl holidays\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," openpyxl",[40,59,60],{"class":50}," holidays\n",[10,62,63],{},"The sample is twelve expense lines for September 2026 with the date column read as text, the way a column with mixed content arrives from Excel. The checker is run on 5 October 2026.",[25,65,67],{"id":66},"step-1-parse-what-can-be-parsed-keep-the-rest-visible","Step 1 — Parse what can be parsed, keep the rest visible",[30,69,73],{"className":70,"code":71,"language":72,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nraw = pd.read_excel(\"expenses_sep.xlsx\", dtype={\"Date\": str})\nperiod = pd.Period(\"2026-09\", freq=\"M\")\ntoday = pd.Timestamp(\"2026-10-05\")\n\niso = pd.to_datetime(raw[\"Date\"], format=\"%Y-%m-%d\", errors=\"coerce\")\ndmy = pd.to_datetime(raw[\"Date\"], format=\"%d\u002F%m\u002F%Y\", errors=\"coerce\")\nserial = pd.to_numeric(raw[\"Date\"], errors=\"coerce\")\nfrom_serial = pd.to_datetime(serial, unit=\"D\", origin=\"1899-12-30\").where(serial.between(20_000, 80_000))\nraw[\"Parsed\"] = iso.fillna(dmy).fillna(from_serial)\nraw[\"How\"] = (iso.notna().map({True: \"ISO\", False: \"\"}) + dmy.notna().map({True: \"d\u002Fm\u002FY\", False: \"\"})\n              + from_serial.notna().map({True: \"Excel serial\", False: \"\"})).replace(\"\", \"not a date\")\nprint(raw[[\"Line\", \"Date\", \"Parsed\", \"How\"]].to_string(index=False))\n","python",[37,74,75,91,98,138,165,181,186,229,264,287,330,347,406,442],{"__ignoreMap":35},[40,76,77,81,85,88],{"class":42,"line":43},[40,78,80],{"class":79},"s-kum","import",[40,82,84],{"class":83},"skGVy"," pandas ",[40,86,87],{"class":79},"as",[40,89,90],{"class":83}," pd\n",[40,92,94],{"class":42,"line":93},2,[40,95,97],{"emptyLinePlaceholder":96},true,"\n",[40,99,101,104,107,110,113,116,120,122,125,128,131,135],{"class":42,"line":100},3,[40,102,103],{"class":83},"raw ",[40,105,106],{"class":79},"=",[40,108,109],{"class":83}," pd.read_excel(",[40,111,112],{"class":50},"\"expenses_sep.xlsx\"",[40,114,115],{"class":83},", ",[40,117,119],{"class":118},"sa561","dtype",[40,121,106],{"class":79},[40,123,124],{"class":83},"{",[40,126,127],{"class":50},"\"Date\"",[40,129,130],{"class":83},": ",[40,132,134],{"class":133},"sP0c6","str",[40,136,137],{"class":83},"})\n",[40,139,141,144,146,149,152,154,157,159,162],{"class":42,"line":140},4,[40,142,143],{"class":83},"period ",[40,145,106],{"class":79},[40,147,148],{"class":83}," pd.Period(",[40,150,151],{"class":50},"\"2026-09\"",[40,153,115],{"class":83},[40,155,156],{"class":118},"freq",[40,158,106],{"class":79},[40,160,161],{"class":50},"\"M\"",[40,163,164],{"class":83},")\n",[40,166,168,171,173,176,179],{"class":42,"line":167},5,[40,169,170],{"class":83},"today ",[40,172,106],{"class":79},[40,174,175],{"class":83}," pd.Timestamp(",[40,177,178],{"class":50},"\"2026-10-05\"",[40,180,164],{"class":83},[40,182,184],{"class":42,"line":183},6,[40,185,97],{"emptyLinePlaceholder":96},[40,187,189,192,194,197,199,202,205,207,210,214,217,219,222,224,227],{"class":42,"line":188},7,[40,190,191],{"class":83},"iso ",[40,193,106],{"class":79},[40,195,196],{"class":83}," pd.to_datetime(raw[",[40,198,127],{"class":50},[40,200,201],{"class":83},"], ",[40,203,204],{"class":118},"format",[40,206,106],{"class":79},[40,208,209],{"class":50},"\"%Y-%m-",[40,211,213],{"class":212},"sSjpA","%d",[40,215,216],{"class":50},"\"",[40,218,115],{"class":83},[40,220,221],{"class":118},"errors",[40,223,106],{"class":79},[40,225,226],{"class":50},"\"coerce\"",[40,228,164],{"class":83},[40,230,232,235,237,239,241,243,245,247,249,251,254,256,258,260,262],{"class":42,"line":231},8,[40,233,234],{"class":83},"dmy ",[40,236,106],{"class":79},[40,238,196],{"class":83},[40,240,127],{"class":50},[40,242,201],{"class":83},[40,244,204],{"class":118},[40,246,106],{"class":79},[40,248,216],{"class":50},[40,250,213],{"class":212},[40,252,253],{"class":50},"\u002F%m\u002F%Y\"",[40,255,115],{"class":83},[40,257,221],{"class":118},[40,259,106],{"class":79},[40,261,226],{"class":50},[40,263,164],{"class":83},[40,265,267,270,272,275,277,279,281,283,285],{"class":42,"line":266},9,[40,268,269],{"class":83},"serial ",[40,271,106],{"class":79},[40,273,274],{"class":83}," pd.to_numeric(raw[",[40,276,127],{"class":50},[40,278,201],{"class":83},[40,280,221],{"class":118},[40,282,106],{"class":79},[40,284,226],{"class":50},[40,286,164],{"class":83},[40,288,290,293,295,298,301,303,306,308,311,313,316,319,322,324,327],{"class":42,"line":289},10,[40,291,292],{"class":83},"from_serial ",[40,294,106],{"class":79},[40,296,297],{"class":83}," pd.to_datetime(serial, ",[40,299,300],{"class":118},"unit",[40,302,106],{"class":79},[40,304,305],{"class":50},"\"D\"",[40,307,115],{"class":83},[40,309,310],{"class":118},"origin",[40,312,106],{"class":79},[40,314,315],{"class":50},"\"1899-12-30\"",[40,317,318],{"class":83},").where(serial.between(",[40,320,321],{"class":133},"20_000",[40,323,115],{"class":83},[40,325,326],{"class":133},"80_000",[40,328,329],{"class":83},"))\n",[40,331,333,336,339,342,344],{"class":42,"line":332},11,[40,334,335],{"class":83},"raw[",[40,337,338],{"class":50},"\"Parsed\"",[40,340,341],{"class":83},"] ",[40,343,106],{"class":79},[40,345,346],{"class":83}," iso.fillna(dmy).fillna(from_serial)\n",[40,348,350,352,355,357,359,362,365,367,370,372,375,377,380,383,386,389,391,393,396,398,400,402,404],{"class":42,"line":349},12,[40,351,335],{"class":83},[40,353,354],{"class":50},"\"How\"",[40,356,341],{"class":83},[40,358,106],{"class":79},[40,360,361],{"class":83}," (iso.notna().map({",[40,363,364],{"class":133},"True",[40,366,130],{"class":83},[40,368,369],{"class":50},"\"ISO\"",[40,371,115],{"class":83},[40,373,374],{"class":133},"False",[40,376,130],{"class":83},[40,378,379],{"class":50},"\"\"",[40,381,382],{"class":83},"}) ",[40,384,385],{"class":79},"+",[40,387,388],{"class":83}," dmy.notna().map({",[40,390,364],{"class":133},[40,392,130],{"class":83},[40,394,395],{"class":50},"\"d\u002Fm\u002FY\"",[40,397,115],{"class":83},[40,399,374],{"class":133},[40,401,130],{"class":83},[40,403,379],{"class":50},[40,405,137],{"class":83},[40,407,409,412,415,417,419,422,424,426,428,430,433,435,437,440],{"class":42,"line":408},13,[40,410,411],{"class":79},"              +",[40,413,414],{"class":83}," from_serial.notna().map({",[40,416,364],{"class":133},[40,418,130],{"class":83},[40,420,421],{"class":50},"\"Excel serial\"",[40,423,115],{"class":83},[40,425,374],{"class":133},[40,427,130],{"class":83},[40,429,379],{"class":50},[40,431,432],{"class":83},"})).replace(",[40,434,379],{"class":50},[40,436,115],{"class":83},[40,438,439],{"class":50},"\"not a date\"",[40,441,164],{"class":83},[40,443,445,448,451,454,456,458,460,462,464,466,469,472,474,476],{"class":42,"line":444},14,[40,446,447],{"class":133},"print",[40,449,450],{"class":83},"(raw[[",[40,452,453],{"class":50},"\"Line\"",[40,455,115],{"class":83},[40,457,127],{"class":50},[40,459,115],{"class":83},[40,461,338],{"class":50},[40,463,115],{"class":83},[40,465,354],{"class":50},[40,467,468],{"class":83},"]].to_string(",[40,470,471],{"class":118},"index",[40,473,106],{"class":79},[40,475,374],{"class":133},[40,477,329],{"class":83},[10,479,480,481,484,485,488],{},"The date column is read as text so that nothing is converted silently on the way in. Each expected format is then tried explicitly: ISO dates, day-first dates with slashes, and plain numbers in the range Excel uses for dates — around 20,000 to 80,000 covers 1954 to 2119 — converted from Excel's serial numbering, which counts days from 30 December 1899. Recording ",[14,482,483],{},"how"," each value was parsed is useful later: a value that only parsed as an Excel serial is a sign the source column was formatted wrongly, and a value that did not parse at all — ",[14,486,487],{},"next week"," — needs a person. Nothing is dropped at this stage; the unparsed row stays in the table with an empty date. In this file nine values are ISO dates, one is day-first, one — 46280 — only makes sense as an Excel serial for 15 September, and one is not a date at all.",[490,491,499,500,499,504,499,508,499,516,499,526,499,533,499,537,499,542,499,548,499,553,499,557,499,562,499,565,499,568,499,572,499,575,499,578,499,582,499,585,499,587,499,591,499,594,499,599,499,604,499,608,499,611,499,615,499,618,499,623,499,628,499,631],"svg",{"viewBox":492,"role":493,"ariaLabelledBy":494,"xmlns":497,"style":498},"4 0 752 108","img",[495,496],"dv-parse-t","dv-parse-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[501,502,503],"title",{"id":495},"How each date value is read",[505,506,507],"desc",{"id":496},"The value is tried as an ISO date, then as a day-first date with slashes, then as an Excel serial number between 20,000 and 80,000 counted from 30 December 1899. Values that match none are kept and marked as not a date.",[509,510],"rect",{"x":511,"y":512,"width":513,"height":514,"fill":515},"4","0","752","108","#ffffff",[509,517],{"x":518,"y":519,"width":520,"height":521,"rx":522,"fill":523,"stroke":524,"style":525},"20.0","16.0","115.2","76.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[527,528,532],"text",{"x":529,"y":530,"style":531},"77.6","41.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Text from",[527,534,536],{"x":529,"y":535,"style":531},"59.0","Excel",[527,538,541],{"x":529,"y":539,"style":540},"78.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","read as str",[42,543],{"x1":544,"y1":545,"x2":546,"y2":545,"stroke":547,"style":525},"140.2","54.2","162.2","var(--brand,#5b5cf0)",[549,550],"polygon",{"points":551,"fill":552},"166.2,54.2 157.2,59.2 157.2,49.2","#5b5cf0",[509,554],{"x":555,"y":519,"width":520,"height":521,"rx":522,"fill":556,"stroke":547,"style":525},"171.2","#f0f4ff",[527,558,561],{"x":559,"y":530,"style":560},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","ISO",[527,563,564],{"x":559,"y":535,"style":560},"yyyy-mm-dd?",[527,566,567],{"x":559,"y":539,"style":540},"2026-09-03",[42,569],{"x1":570,"y1":545,"x2":571,"y2":545,"stroke":547,"style":525},"291.4","313.4",[549,573],{"points":574,"fill":552},"317.4,54.2 308.4,59.2 308.4,49.2",[509,576],{"x":577,"y":519,"width":520,"height":521,"rx":522,"fill":556,"stroke":547,"style":525},"322.4",[527,579,581],{"x":580,"y":530,"style":560},"380.0","Day-first",[527,583,584],{"x":580,"y":535,"style":560},"d\u002Fm\u002FY?",[527,586,16],{"x":580,"y":539,"style":540},[42,588],{"x1":589,"y1":545,"x2":590,"y2":545,"stroke":547,"style":525},"442.6","464.6",[549,592],{"points":593,"fill":552},"468.6,54.2 459.6,59.2 459.6,49.2",[509,595],{"x":596,"y":519,"width":520,"height":521,"rx":522,"fill":597,"stroke":598,"style":525},"473.6","#fdefd8","var(--gold,#b4740a)",[527,600,603],{"x":601,"y":530,"style":602},"531.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Excel serial?",[527,605,607],{"x":601,"y":606,"style":540},"61.0","46280 → 15",[527,609,610],{"x":601,"y":539,"style":540},"Sep 2026",[42,612],{"x1":613,"y1":545,"x2":614,"y2":545,"stroke":547,"style":525},"593.8","615.8",[549,616],{"points":617,"fill":552},"619.8,54.2 610.8,59.2 610.8,49.2",[509,619],{"x":620,"y":519,"width":520,"height":521,"rx":522,"fill":621,"stroke":622,"style":525},"624.8","#fce9e9","var(--danger,#dc2626)",[527,624,627],{"x":625,"y":530,"style":626},"682.4","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","None of",[527,629,630],{"x":625,"y":535,"style":626},"these",[527,632,633],{"x":625,"y":539,"style":540},"not a date",[25,635,637],{"id":636},"step-2-tag-each-problem-with-a-reason","Step 2 — Tag each problem with a reason",[30,639,641],{"className":70,"code":640,"language":72,"meta":35,"style":35},"import holidays\n\nbank = holidays.country_holidays(\"GB\", subdiv=\"ENG\", years=[2026])\nstart, end = period.start_time, period.end_time\n\ndef reasons(row):\n    d, out = row[\"Parsed\"], []\n    if pd.isna(d):\n        return [\"not a date\"]\n    if d.year \u003C 2000 or d.year > today.year + 1:\n        out.append(\"implausible year\")\n    elif d > today:\n        out.append(\"in the future\")\n    if not (start \u003C= d \u003C= end) and \"implausible year\" not in out:\n        out.append(\"outside the period\")\n    if d.weekday() >= 5 or d.date() in bank:\n        out.append(\"non-working day\")\n    if row[\"How\"] == \"Excel serial\":\n        out.append(\"arrived as a number\")\n    return out\n\nraw[\"Problems\"] = raw.apply(reasons, axis=1)\nflagged = raw[raw[\"Problems\"].str.len() > 0]\nprint(flagged[[\"Line\", \"Date\", \"Parsed\", \"Problems\"]].to_string(index=False))\n",[37,642,643,649,653,692,702,706,718,733,741,754,786,796,809,818,852,862,887,897,916,926,935,940,965,988],{"__ignoreMap":35},[40,644,645,647],{"class":42,"line":43},[40,646,80],{"class":79},[40,648,60],{"class":83},[40,650,651],{"class":42,"line":93},[40,652,97],{"emptyLinePlaceholder":96},[40,654,655,658,660,663,666,668,671,673,676,678,681,683,686,689],{"class":42,"line":100},[40,656,657],{"class":83},"bank ",[40,659,106],{"class":79},[40,661,662],{"class":83}," holidays.country_holidays(",[40,664,665],{"class":50},"\"GB\"",[40,667,115],{"class":83},[40,669,670],{"class":118},"subdiv",[40,672,106],{"class":79},[40,674,675],{"class":50},"\"ENG\"",[40,677,115],{"class":83},[40,679,680],{"class":118},"years",[40,682,106],{"class":79},[40,684,685],{"class":83},"[",[40,687,688],{"class":133},"2026",[40,690,691],{"class":83},"])\n",[40,693,694,697,699],{"class":42,"line":140},[40,695,696],{"class":83},"start, end ",[40,698,106],{"class":79},[40,700,701],{"class":83}," period.start_time, period.end_time\n",[40,703,704],{"class":42,"line":167},[40,705,97],{"emptyLinePlaceholder":96},[40,707,708,711,715],{"class":42,"line":183},[40,709,710],{"class":79},"def",[40,712,714],{"class":713},"s_Opv"," reasons",[40,716,717],{"class":83},"(row):\n",[40,719,720,723,725,728,730],{"class":42,"line":188},[40,721,722],{"class":83},"    d, out ",[40,724,106],{"class":79},[40,726,727],{"class":83}," row[",[40,729,338],{"class":50},[40,731,732],{"class":83},"], []\n",[40,734,735,738],{"class":42,"line":231},[40,736,737],{"class":79},"    if",[40,739,740],{"class":83}," pd.isna(d):\n",[40,742,743,746,749,751],{"class":42,"line":266},[40,744,745],{"class":79},"        return",[40,747,748],{"class":83}," [",[40,750,439],{"class":50},[40,752,753],{"class":83},"]\n",[40,755,756,758,761,764,767,770,772,775,778,780,783],{"class":42,"line":289},[40,757,737],{"class":79},[40,759,760],{"class":83}," d.year ",[40,762,763],{"class":79},"\u003C",[40,765,766],{"class":133}," 2000",[40,768,769],{"class":79}," or",[40,771,760],{"class":83},[40,773,774],{"class":79},">",[40,776,777],{"class":83}," today.year ",[40,779,385],{"class":79},[40,781,782],{"class":133}," 1",[40,784,785],{"class":83},":\n",[40,787,788,791,794],{"class":42,"line":332},[40,789,790],{"class":83},"        out.append(",[40,792,793],{"class":50},"\"implausible year\"",[40,795,164],{"class":83},[40,797,798,801,804,806],{"class":42,"line":349},[40,799,800],{"class":79},"    elif",[40,802,803],{"class":83}," d ",[40,805,774],{"class":79},[40,807,808],{"class":83}," today:\n",[40,810,811,813,816],{"class":42,"line":408},[40,812,790],{"class":83},[40,814,815],{"class":50},"\"in the future\"",[40,817,164],{"class":83},[40,819,820,822,825,828,831,833,835,838,841,844,846,849],{"class":42,"line":444},[40,821,737],{"class":79},[40,823,824],{"class":79}," not",[40,826,827],{"class":83}," (start ",[40,829,830],{"class":79},"\u003C=",[40,832,803],{"class":83},[40,834,830],{"class":79},[40,836,837],{"class":83}," end) ",[40,839,840],{"class":79},"and",[40,842,843],{"class":50}," \"implausible year\"",[40,845,824],{"class":79},[40,847,848],{"class":79}," in",[40,850,851],{"class":83}," out:\n",[40,853,855,857,860],{"class":42,"line":854},15,[40,856,790],{"class":83},[40,858,859],{"class":50},"\"outside the period\"",[40,861,164],{"class":83},[40,863,865,867,870,873,876,878,881,884],{"class":42,"line":864},16,[40,866,737],{"class":79},[40,868,869],{"class":83}," d.weekday() ",[40,871,872],{"class":79},">=",[40,874,875],{"class":133}," 5",[40,877,769],{"class":79},[40,879,880],{"class":83}," d.date() ",[40,882,883],{"class":79},"in",[40,885,886],{"class":83}," bank:\n",[40,888,890,892,895],{"class":42,"line":889},17,[40,891,790],{"class":83},[40,893,894],{"class":50},"\"non-working day\"",[40,896,164],{"class":83},[40,898,900,902,904,906,908,911,914],{"class":42,"line":899},18,[40,901,737],{"class":79},[40,903,727],{"class":83},[40,905,354],{"class":50},[40,907,341],{"class":83},[40,909,910],{"class":79},"==",[40,912,913],{"class":50}," \"Excel serial\"",[40,915,785],{"class":83},[40,917,919,921,924],{"class":42,"line":918},19,[40,920,790],{"class":83},[40,922,923],{"class":50},"\"arrived as a number\"",[40,925,164],{"class":83},[40,927,929,932],{"class":42,"line":928},20,[40,930,931],{"class":79},"    return",[40,933,934],{"class":83}," out\n",[40,936,938],{"class":42,"line":937},21,[40,939,97],{"emptyLinePlaceholder":96},[40,941,943,945,948,950,952,955,958,960,963],{"class":42,"line":942},22,[40,944,335],{"class":83},[40,946,947],{"class":50},"\"Problems\"",[40,949,341],{"class":83},[40,951,106],{"class":79},[40,953,954],{"class":83}," raw.apply(reasons, ",[40,956,957],{"class":118},"axis",[40,959,106],{"class":79},[40,961,962],{"class":133},"1",[40,964,164],{"class":83},[40,966,968,971,973,976,978,981,983,986],{"class":42,"line":967},23,[40,969,970],{"class":83},"flagged ",[40,972,106],{"class":79},[40,974,975],{"class":83}," raw[raw[",[40,977,947],{"class":50},[40,979,980],{"class":83},"].str.len() ",[40,982,774],{"class":79},[40,984,985],{"class":133}," 0",[40,987,753],{"class":83},[40,989,991,993,996,998,1000,1002,1004,1006,1008,1010,1012,1014,1016,1018],{"class":42,"line":990},24,[40,992,447],{"class":133},[40,994,995],{"class":83},"(flagged[[",[40,997,453],{"class":50},[40,999,115],{"class":83},[40,1001,127],{"class":50},[40,1003,115],{"class":83},[40,1005,338],{"class":50},[40,1007,115],{"class":83},[40,1009,947],{"class":50},[40,1011,468],{"class":83},[40,1013,471],{"class":118},[40,1015,106],{"class":79},[40,1017,374],{"class":133},[40,1019,329],{"class":83},[10,1021,1022,1023,1026,1027,1030,1031,1034,1035,1038],{},"Each rule describes one kind of problem, and a row can have several. A year before 2000 or more than a year ahead is ",[14,1024,1025],{},"implausible"," — almost always a typing slip rather than a real expense from 1926. Any other date after today is ",[14,1028,1029],{},"in the future",". Dates outside September are ",[14,1032,1033],{},"outside the period",". Weekends and public holidays are flagged as ",[14,1036,1037],{},"non-working days",", which is not an error for every kind of data but is worth a look in an expense or timesheet import. And a value that only made sense as an Excel serial number is flagged even when its date is fine, because the file it came from needs fixing. Returning a list of reasons per row, rather than a single yes or no, is what makes the output useful to the person who has to fix it.",[25,1040,1042],{"id":1041},"step-3-suggest-corrections-where-the-evidence-is-clear","Step 3 — Suggest corrections where the evidence is clear",[30,1044,1046],{"className":70,"code":1045,"language":72,"meta":35,"style":35},"def suggestion(row):\n    d = row[\"Parsed\"]\n    if pd.isna(d):\n        return None\n    if \"implausible year\" in row[\"Problems\"]:\n        for year in (period.year, period.year - 1):\n            try:\n                alt = d.replace(year=year)\n            except ValueError:\n                continue\n            if start \u003C= alt \u003C= end:\n                return alt.date()\n    if row[\"How\"] == \"d\u002Fm\u002FY\" and d.day \u003C= 12:\n        swapped = d.replace(day=d.month, month=d.day)\n        if start \u003C= swapped \u003C= end and not (start \u003C= d \u003C= end):\n            return swapped.date()\n    return None\n\nraw[\"Suggested\"] = raw.apply(suggestion, axis=1)\nout = raw.loc[raw[\"Problems\"].str.len() > 0, [\"Line\", \"Date\", \"Amount\", \"Problems\", \"Suggested\"]]\nout[\"Problems\"] = out[\"Problems\"].str.join(\"; \")\nprint(out.to_string(index=False))\n",[37,1047,1048,1057,1070,1076,1083,1098,1119,1126,1144,1154,1159,1177,1185,1213,1238,1270,1278,1284,1288,1310,1353,1377],{"__ignoreMap":35},[40,1049,1050,1052,1055],{"class":42,"line":43},[40,1051,710],{"class":79},[40,1053,1054],{"class":713}," suggestion",[40,1056,717],{"class":83},[40,1058,1059,1062,1064,1066,1068],{"class":42,"line":93},[40,1060,1061],{"class":83},"    d ",[40,1063,106],{"class":79},[40,1065,727],{"class":83},[40,1067,338],{"class":50},[40,1069,753],{"class":83},[40,1071,1072,1074],{"class":42,"line":100},[40,1073,737],{"class":79},[40,1075,740],{"class":83},[40,1077,1078,1080],{"class":42,"line":140},[40,1079,745],{"class":79},[40,1081,1082],{"class":133}," None\n",[40,1084,1085,1087,1089,1091,1093,1095],{"class":42,"line":167},[40,1086,737],{"class":79},[40,1088,843],{"class":50},[40,1090,848],{"class":79},[40,1092,727],{"class":83},[40,1094,947],{"class":50},[40,1096,1097],{"class":83},"]:\n",[40,1099,1100,1103,1106,1108,1111,1114,1116],{"class":42,"line":183},[40,1101,1102],{"class":79},"        for",[40,1104,1105],{"class":83}," year ",[40,1107,883],{"class":79},[40,1109,1110],{"class":83}," (period.year, period.year ",[40,1112,1113],{"class":79},"-",[40,1115,782],{"class":133},[40,1117,1118],{"class":83},"):\n",[40,1120,1121,1124],{"class":42,"line":188},[40,1122,1123],{"class":79},"            try",[40,1125,785],{"class":83},[40,1127,1128,1131,1133,1136,1139,1141],{"class":42,"line":231},[40,1129,1130],{"class":83},"                alt ",[40,1132,106],{"class":79},[40,1134,1135],{"class":83}," d.replace(",[40,1137,1138],{"class":118},"year",[40,1140,106],{"class":79},[40,1142,1143],{"class":83},"year)\n",[40,1145,1146,1149,1152],{"class":42,"line":266},[40,1147,1148],{"class":79},"            except",[40,1150,1151],{"class":133}," ValueError",[40,1153,785],{"class":83},[40,1155,1156],{"class":42,"line":289},[40,1157,1158],{"class":79},"                continue\n",[40,1160,1161,1164,1167,1169,1172,1174],{"class":42,"line":332},[40,1162,1163],{"class":79},"            if",[40,1165,1166],{"class":83}," start ",[40,1168,830],{"class":79},[40,1170,1171],{"class":83}," alt ",[40,1173,830],{"class":79},[40,1175,1176],{"class":83}," end:\n",[40,1178,1179,1182],{"class":42,"line":349},[40,1180,1181],{"class":79},"                return",[40,1183,1184],{"class":83}," alt.date()\n",[40,1186,1187,1189,1191,1193,1195,1197,1200,1203,1206,1208,1211],{"class":42,"line":408},[40,1188,737],{"class":79},[40,1190,727],{"class":83},[40,1192,354],{"class":50},[40,1194,341],{"class":83},[40,1196,910],{"class":79},[40,1198,1199],{"class":50}," \"d\u002Fm\u002FY\"",[40,1201,1202],{"class":79}," and",[40,1204,1205],{"class":83}," d.day ",[40,1207,830],{"class":79},[40,1209,1210],{"class":133}," 12",[40,1212,785],{"class":83},[40,1214,1215,1218,1220,1222,1225,1227,1230,1233,1235],{"class":42,"line":444},[40,1216,1217],{"class":83},"        swapped ",[40,1219,106],{"class":79},[40,1221,1135],{"class":83},[40,1223,1224],{"class":118},"day",[40,1226,106],{"class":79},[40,1228,1229],{"class":83},"d.month, ",[40,1231,1232],{"class":118},"month",[40,1234,106],{"class":79},[40,1236,1237],{"class":83},"d.day)\n",[40,1239,1240,1243,1245,1247,1250,1252,1255,1257,1259,1261,1263,1265,1267],{"class":42,"line":854},[40,1241,1242],{"class":79},"        if",[40,1244,1166],{"class":83},[40,1246,830],{"class":79},[40,1248,1249],{"class":83}," swapped ",[40,1251,830],{"class":79},[40,1253,1254],{"class":83}," end ",[40,1256,840],{"class":79},[40,1258,824],{"class":79},[40,1260,827],{"class":83},[40,1262,830],{"class":79},[40,1264,803],{"class":83},[40,1266,830],{"class":79},[40,1268,1269],{"class":83}," end):\n",[40,1271,1272,1275],{"class":42,"line":864},[40,1273,1274],{"class":79},"            return",[40,1276,1277],{"class":83}," swapped.date()\n",[40,1279,1280,1282],{"class":42,"line":889},[40,1281,931],{"class":79},[40,1283,1082],{"class":133},[40,1285,1286],{"class":42,"line":899},[40,1287,97],{"emptyLinePlaceholder":96},[40,1289,1290,1292,1295,1297,1299,1302,1304,1306,1308],{"class":42,"line":918},[40,1291,335],{"class":83},[40,1293,1294],{"class":50},"\"Suggested\"",[40,1296,341],{"class":83},[40,1298,106],{"class":79},[40,1300,1301],{"class":83}," raw.apply(suggestion, ",[40,1303,957],{"class":118},[40,1305,106],{"class":79},[40,1307,962],{"class":133},[40,1309,164],{"class":83},[40,1311,1312,1315,1317,1320,1322,1324,1326,1328,1331,1333,1335,1337,1339,1342,1344,1346,1348,1350],{"class":42,"line":928},[40,1313,1314],{"class":83},"out ",[40,1316,106],{"class":79},[40,1318,1319],{"class":83}," raw.loc[raw[",[40,1321,947],{"class":50},[40,1323,980],{"class":83},[40,1325,774],{"class":79},[40,1327,985],{"class":133},[40,1329,1330],{"class":83},", [",[40,1332,453],{"class":50},[40,1334,115],{"class":83},[40,1336,127],{"class":50},[40,1338,115],{"class":83},[40,1340,1341],{"class":50},"\"Amount\"",[40,1343,115],{"class":83},[40,1345,947],{"class":50},[40,1347,115],{"class":83},[40,1349,1294],{"class":50},[40,1351,1352],{"class":83},"]]\n",[40,1354,1355,1358,1360,1362,1364,1367,1369,1372,1375],{"class":42,"line":937},[40,1356,1357],{"class":83},"out[",[40,1359,947],{"class":50},[40,1361,341],{"class":83},[40,1363,106],{"class":79},[40,1365,1366],{"class":83}," out[",[40,1368,947],{"class":50},[40,1370,1371],{"class":83},"].str.join(",[40,1373,1374],{"class":50},"\"; \"",[40,1376,164],{"class":83},[40,1378,1379,1381,1384,1386,1388,1390],{"class":42,"line":942},[40,1380,447],{"class":133},[40,1382,1383],{"class":83},"(out.to_string(",[40,1385,471],{"class":118},[40,1387,106],{"class":79},[40,1389,374],{"class":133},[40,1391,329],{"class":83},[10,1393,1394,1395,1398,1399,1402,1403,1405],{},"Some problems have an obvious fix. A date whose year is implausible but whose day and month fall in the reporting month almost certainly has a mistyped year — ",[14,1396,1397],{},"2062-09-21"," becomes 21 September 2026, and ",[14,1400,1401],{},"1926-09-14"," becomes 14 September 2026. A date that parsed day-first and fell outside the period, but would fall inside it with day and month swapped, was probably entered month-first. In this file the day-first ",[14,1404,16],{}," is 3 September, inside the period, so it is left alone — swapping it would be guessing. Suggestions are only offered when they land inside the reporting period; everything else is flagged for a person to decide, and no suggestion is applied automatically.",[490,1407,499,1413,499,1416,499,1419,499,1424,499,1430,499,1434,499,1440,499,1444,499,1448,499,1452,499,1456,499,1460,499,1464,499,1468,499,1471,499,1474,499,1478,499,1480,499,1483,499,1485,499,1488,499,1490,499,1493,499,1496,499,1500,499,1502,499,1506,499,1508,499,1510,499,1512,499,1515,499,1519,499,1523,499,1526,499,1529,499,1531,499,1533,499,1535,499,1538,499,1540,499,1543,499,1545,499,1548,499,1551,499,1553,499,1556,499,1558,499,1561,499,1563,499,1565,499,1567,499,1570,499,1573,499,1575,499,1578,499,1580,499,1583,499,1585,499,1588,499,1590,499,1593,499,1597,499,1599,499,1602,499,1604,499,1607,499,1609,499,1612,499,1614,499,1617,499,1620,499,1622,499,1625,499,1627,499,1629,499,1631,499,1634,499,1636,499,1639,499,1642,499,1644,499,1647,499,1649,499,1651,499,1653,499,1655,499,1657],{"viewBox":1408,"role":493,"ariaLabelledBy":1409,"xmlns":497,"style":1412},"98 0 563 280",[1410,1411],"dv-results-t","dv-results-d","width:100%;max-width:563px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[501,1414,1415],{"id":1410},"The flagged lines",[505,1417,1418],{"id":1411},"Line 3 dated 2062 has an implausible year and a suggested date of 21 September 2026. Line 4 from 29 August is outside the period and a Saturday. Line 5 from 2 October is outside the period. Line 7 arrived as an Excel serial. Line 8 falls on a Saturday. Line 9 is not a date. Line 11 dated 1926 is suggested as 14 September 2026.",[509,1420],{"x":1421,"y":512,"width":1422,"height":1423,"fill":515},"98","563","280",[509,1425],{"x":1426,"y":519,"width":1427,"height":1428,"rx":512,"fill":523,"stroke":524,"style":1429},"114.9","32.0","24.0","stroke-width:1px",[509,1431],{"x":1432,"y":519,"width":1433,"height":1428,"rx":512,"fill":523,"stroke":524,"style":1429},"146.9","49.9",[527,1435,1439],{"x":1436,"y":1437,"style":1438},"171.8","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[509,1441],{"x":1442,"y":519,"width":1443,"height":1428,"rx":512,"fill":523,"stroke":524,"style":1429},"196.8","101.1",[527,1445,1447],{"x":1446,"y":1437,"style":1438},"247.3","B",[509,1449],{"x":1450,"y":519,"width":1451,"height":1428,"rx":512,"fill":523,"stroke":524,"style":1429},"297.9","248.3",[527,1453,1455],{"x":1454,"y":1437,"style":1438},"422.0","C",[509,1457],{"x":1458,"y":519,"width":1459,"height":1428,"rx":512,"fill":523,"stroke":524,"style":1429},"546.2","99.0",[527,1461,1463],{"x":1462,"y":1437,"style":1438},"595.6","D",[509,1465],{"x":1426,"y":1466,"width":1427,"height":1467,"rx":512,"fill":523,"stroke":524,"style":1429},"40.0","28.0",[527,1469,962],{"x":1470,"y":535,"style":1438},"130.9",[509,1472],{"x":1432,"y":1466,"width":1433,"height":1467,"rx":512,"fill":556,"stroke":547,"style":1473},"stroke-width:1.5px",[527,1475,1477],{"x":1436,"y":535,"style":1476},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Line",[509,1479],{"x":1442,"y":1466,"width":1443,"height":1467,"rx":512,"fill":556,"stroke":547,"style":1473},[527,1481,1482],{"x":1446,"y":535,"style":1476},"Date",[509,1484],{"x":1450,"y":1466,"width":1451,"height":1467,"rx":512,"fill":556,"stroke":547,"style":1473},[527,1486,1487],{"x":1454,"y":535,"style":1476},"Problems",[509,1489],{"x":1458,"y":1466,"width":1459,"height":1467,"rx":512,"fill":556,"stroke":547,"style":1473},[527,1491,1492],{"x":1462,"y":535,"style":1476},"Suggested",[509,1494],{"x":1426,"y":1495,"width":1427,"height":1467,"rx":512,"fill":523,"stroke":524,"style":1429},"68.0",[527,1497,1499],{"x":1470,"y":1498,"style":1438},"87.0","2",[509,1501],{"x":1432,"y":1495,"width":1433,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1503,1505],{"x":1436,"y":1498,"style":1504},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","3",[509,1507],{"x":1442,"y":1495,"width":1443,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1509,1397],{"x":1446,"y":1498,"style":1504},[509,1511],{"x":1450,"y":1495,"width":1451,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1513,1514],{"x":1454,"y":1498,"style":1504},"implausible year",[509,1516],{"x":1458,"y":1495,"width":1459,"height":1467,"rx":512,"fill":1517,"stroke":1518,"style":1473},"#d9f4f1","var(--teal,#0f9488)",[527,1520,1522],{"x":1462,"y":1498,"style":1521},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","2026-09-21",[509,1524],{"x":1426,"y":1525,"width":1427,"height":1467,"rx":512,"fill":523,"stroke":524,"style":1429},"96.0",[527,1527,1505],{"x":1470,"y":1528,"style":1438},"115.0",[509,1530],{"x":1432,"y":1525,"width":1433,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1532,511],{"x":1436,"y":1528,"style":1504},[509,1534],{"x":1442,"y":1525,"width":1443,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1536,1537],{"x":1446,"y":1528,"style":1504},"2026-08-29",[509,1539],{"x":1450,"y":1525,"width":1451,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1541,1542],{"x":1454,"y":1528,"style":1504},"outside the period; non-working day",[509,1544],{"x":1458,"y":1525,"width":1459,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[509,1546],{"x":1426,"y":1547,"width":1427,"height":1467,"rx":512,"fill":523,"stroke":524,"style":1429},"124.0",[527,1549,511],{"x":1470,"y":1550,"style":1438},"143.0",[509,1552],{"x":1432,"y":1547,"width":1433,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1554,1555],{"x":1436,"y":1550,"style":1504},"5",[509,1557],{"x":1442,"y":1547,"width":1443,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1559,1560],{"x":1446,"y":1550,"style":1504},"2026-10-02",[509,1562],{"x":1450,"y":1547,"width":1451,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1564,1033],{"x":1454,"y":1550,"style":1504},[509,1566],{"x":1458,"y":1547,"width":1459,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[509,1568],{"x":1426,"y":1569,"width":1427,"height":1467,"rx":512,"fill":523,"stroke":524,"style":1429},"152.0",[527,1571,1555],{"x":1470,"y":1572,"style":1438},"171.0",[509,1574],{"x":1432,"y":1569,"width":1433,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1576,1577],{"x":1436,"y":1572,"style":1504},"7",[509,1579],{"x":1442,"y":1569,"width":1443,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1581,1582],{"x":1446,"y":1572,"style":1504},"46280",[509,1584],{"x":1450,"y":1569,"width":1451,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1586,1587],{"x":1454,"y":1572,"style":1504},"arrived as a number",[509,1589],{"x":1458,"y":1569,"width":1459,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[509,1591],{"x":1426,"y":1592,"width":1427,"height":1467,"rx":512,"fill":523,"stroke":524,"style":1429},"180.0",[527,1594,1596],{"x":1470,"y":1595,"style":1438},"199.0","6",[509,1598],{"x":1432,"y":1592,"width":1433,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1600,1601],{"x":1436,"y":1595,"style":1504},"8",[509,1603],{"x":1442,"y":1592,"width":1443,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1605,1606],{"x":1446,"y":1595,"style":1504},"2026-09-26",[509,1608],{"x":1450,"y":1592,"width":1451,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1610,1611],{"x":1454,"y":1595,"style":1504},"non-working day",[509,1613],{"x":1458,"y":1592,"width":1459,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[509,1615],{"x":1426,"y":1616,"width":1427,"height":1467,"rx":512,"fill":523,"stroke":524,"style":1429},"208.0",[527,1618,1577],{"x":1470,"y":1619,"style":1438},"227.0",[509,1621],{"x":1432,"y":1616,"width":1433,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1623,1624],{"x":1436,"y":1619,"style":1504},"9",[509,1626],{"x":1442,"y":1616,"width":1443,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1628,487],{"x":1446,"y":1619,"style":1504},[509,1630],{"x":1450,"y":1616,"width":1451,"height":1467,"rx":512,"fill":621,"stroke":622,"style":1473},[527,1632,633],{"x":1454,"y":1619,"style":1633},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[509,1635],{"x":1458,"y":1616,"width":1459,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[509,1637],{"x":1426,"y":1638,"width":1427,"height":1467,"rx":512,"fill":523,"stroke":524,"style":1429},"236.0",[527,1640,1601],{"x":1470,"y":1641,"style":1438},"255.0",[509,1643],{"x":1432,"y":1638,"width":1433,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1645,1646],{"x":1436,"y":1641,"style":1504},"11",[509,1648],{"x":1442,"y":1638,"width":1443,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1650,1401],{"x":1446,"y":1641,"style":1504},[509,1652],{"x":1450,"y":1638,"width":1451,"height":1467,"rx":512,"fill":515,"stroke":524,"style":1429},[527,1654,1514],{"x":1454,"y":1641,"style":1504},[509,1656],{"x":1458,"y":1638,"width":1459,"height":1467,"rx":512,"fill":1517,"stroke":1518,"style":1473},[527,1658,1659],{"x":1462,"y":1641,"style":1521},"2026-09-14",[25,1661,1663],{"id":1662},"errors-and-warnings","Errors and warnings",[10,1665,1666,1667,1670,1671,1674],{},"Not every flag means the same thing. A date that is not a date, or that falls outside the period, is an ",[14,1668,1669],{},"error",": the row cannot be loaded into September as it stands. A Saturday expense or a value that arrived as a serial number is a ",[14,1672,1673],{},"warning",": the date may well be right — people do buy train tickets at weekends — but someone should glance at it. Treating every flag as an error makes the validator so strict that people stop reading its output; treating none as errors lets bad rows through. A simple severity per reason, used to decide which rows are held back and which are loaded with a note, keeps the balance.",[490,1676,499,1681,499,1683,499,1686,499,1689,499,1694,499,1700,499,1705,499,1708,499,1711,499,1714,499,1717,499,1720,499,1725,499,1729,499,1731],{"viewBox":1677,"role":493,"ariaLabelledBy":1678,"xmlns":497,"style":498},"4 0 752 199",[1679,1680],"dv-severity-t","dv-severity-d",[501,1682,1663],{"id":1679},[505,1684,1685],{"id":1680},"Not a date, implausible year, in the future and outside the period are errors, and the row is held back. A non-working day or a value that arrived as an Excel serial number is a warning; the row can be loaded with a note for review.",[509,1687],{"x":511,"y":512,"width":513,"height":1688,"fill":515},"199",[509,1690],{"x":518,"y":519,"width":1691,"height":1692,"rx":1693,"fill":621,"stroke":622,"style":525},"351.0","167.0","14",[527,1695,1699],{"x":1696,"y":1697,"style":1698},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Error — hold the row back",[42,1701],{"x1":1702,"y1":1703,"x2":1704,"y2":1703,"stroke":622,"style":1429},"36.0","57.0","355.0",[527,1706,633],{"x":1696,"y":1707,"style":1504},"77.0",[527,1709,1514],{"x":1696,"y":1710,"style":1504},"103.0",[527,1712,1029],{"x":1696,"y":1713,"style":1504},"129.0",[527,1715,1033],{"x":1696,"y":1716,"style":1504},"155.0",[509,1718],{"x":1719,"y":519,"width":1691,"height":1692,"rx":1693,"fill":597,"stroke":598,"style":525},"389.0",[527,1721,1724],{"x":1722,"y":1697,"style":1723},"564.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Warning — load with a note",[42,1726],{"x1":1727,"y1":1703,"x2":1728,"y2":1703,"stroke":598,"style":1429},"405.0","724.0",[527,1730,1611],{"x":1722,"y":1707,"style":1504},[527,1732,1587],{"x":1722,"y":1710,"style":1504},[10,1734,1735],{},"With that split, the Saturday expense on line 8 and the serial-number date on line 7 would be loaded with a note, and only five lines would wait for a correction.",[25,1737,1739],{"id":1738},"step-4-load-the-clean-rows-return-the-rest","Step 4 — Load the clean rows, return the rest",[30,1741,1743],{"className":70,"code":1742,"language":72,"meta":35,"style":35},"clean = raw[raw[\"Problems\"].str.len() == 0]\nwith pd.ExcelWriter(\"expenses_sep_validation.xlsx\") as xw:\n    out.to_excel(xw, sheet_name=\"Needs attention\", index=False)\n    clean[[\"Line\", \"Parsed\", \"Amount\"]].rename(columns={\"Parsed\": \"Date\"}).to_excel(xw, sheet_name=\"Ready to load\", index=False)\nprint(f\"{len(clean)} rows ready, {len(out)} need attention, value held back: {out['Amount'].sum():,.2f}\")\n",[37,1744,1745,1764,1783,1806,1857],{"__ignoreMap":35},[40,1746,1747,1750,1752,1754,1756,1758,1760,1762],{"class":42,"line":43},[40,1748,1749],{"class":83},"clean ",[40,1751,106],{"class":79},[40,1753,975],{"class":83},[40,1755,947],{"class":50},[40,1757,980],{"class":83},[40,1759,910],{"class":79},[40,1761,985],{"class":133},[40,1763,753],{"class":83},[40,1765,1766,1769,1772,1775,1778,1780],{"class":42,"line":93},[40,1767,1768],{"class":79},"with",[40,1770,1771],{"class":83}," pd.ExcelWriter(",[40,1773,1774],{"class":50},"\"expenses_sep_validation.xlsx\"",[40,1776,1777],{"class":83},") ",[40,1779,87],{"class":79},[40,1781,1782],{"class":83}," xw:\n",[40,1784,1785,1788,1791,1793,1796,1798,1800,1802,1804],{"class":42,"line":100},[40,1786,1787],{"class":83},"    out.to_excel(xw, ",[40,1789,1790],{"class":118},"sheet_name",[40,1792,106],{"class":79},[40,1794,1795],{"class":50},"\"Needs attention\"",[40,1797,115],{"class":83},[40,1799,471],{"class":118},[40,1801,106],{"class":79},[40,1803,374],{"class":133},[40,1805,164],{"class":83},[40,1807,1808,1811,1813,1815,1817,1819,1821,1824,1827,1829,1831,1833,1835,1837,1840,1842,1844,1847,1849,1851,1853,1855],{"class":42,"line":140},[40,1809,1810],{"class":83},"    clean[[",[40,1812,453],{"class":50},[40,1814,115],{"class":83},[40,1816,338],{"class":50},[40,1818,115],{"class":83},[40,1820,1341],{"class":50},[40,1822,1823],{"class":83},"]].rename(",[40,1825,1826],{"class":118},"columns",[40,1828,106],{"class":79},[40,1830,124],{"class":83},[40,1832,338],{"class":50},[40,1834,130],{"class":83},[40,1836,127],{"class":50},[40,1838,1839],{"class":83},"}).to_excel(xw, ",[40,1841,1790],{"class":118},[40,1843,106],{"class":79},[40,1845,1846],{"class":50},"\"Ready to load\"",[40,1848,115],{"class":83},[40,1850,471],{"class":118},[40,1852,106],{"class":79},[40,1854,374],{"class":133},[40,1856,164],{"class":83},[40,1858,1859,1861,1864,1867,1869,1871,1874,1877,1880,1883,1885,1887,1890,1892,1895,1897,1899,1902,1905,1908,1910,1912],{"class":42,"line":167},[40,1860,447],{"class":133},[40,1862,1863],{"class":83},"(",[40,1865,1866],{"class":79},"f",[40,1868,216],{"class":50},[40,1870,124],{"class":212},[40,1872,1873],{"class":133},"len",[40,1875,1876],{"class":83},"(clean)",[40,1878,1879],{"class":212},"}",[40,1881,1882],{"class":50}," rows ready, ",[40,1884,124],{"class":212},[40,1886,1873],{"class":133},[40,1888,1889],{"class":83},"(out)",[40,1891,1879],{"class":212},[40,1893,1894],{"class":50}," need attention, value held back: ",[40,1896,124],{"class":212},[40,1898,1357],{"class":83},[40,1900,1901],{"class":50},"'Amount'",[40,1903,1904],{"class":83},"].sum()",[40,1906,1907],{"class":79},":,.2f",[40,1909,1879],{"class":212},[40,1911,216],{"class":50},[40,1913,164],{"class":83},[10,1915,1916,1917,1921],{},"The validator splits the file into rows ready to load and rows that need attention, and writes both to a workbook the submitter can work through. Showing the value held back alongside the count matters: seven lines worth 694.80, as here, is a conversation with the submitter; seven lines worth a hundred thousand is an escalation. Rows with a suggested date still go to the attention list, because the decision to accept a correction belongs to the person who knows the expense. For dates that may be day-first or month-first throughout a whole file, the approach in ",[19,1918,1920],{"href":1919},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-mixed-day-first-and-month-first-dates-in-excel-data\u002F","Fix Mixed Day-First and Month-First Dates in Excel Data"," works out the convention from the data as a whole.",[25,1923,1925],{"id":1924},"common-pitfalls","Common pitfalls",[1927,1928,1929,1937,1945,1951,1957],"ul",{},[1930,1931,1932,1936],"li",{},[1933,1934,1935],"strong",{},"Letting pandas parse on read."," Read dates as text and parse with explicit formats.",[1930,1938,1939,1942,1943,23],{},[1933,1940,1941],{},"Dropping unparseable values."," Keep them, tagged ",[14,1944,633],{},[1930,1946,1947,1950],{},[1933,1948,1949],{},"One pass\u002Ffail flag."," Give each row the list of reasons.",[1930,1952,1953,1956],{},[1933,1954,1955],{},"Applying suggested fixes automatically."," Suggest only; let a person accept them.",[1930,1958,1959,1962],{},[1933,1960,1961],{},"Ignoring Excel serial numbers."," Convert them, and flag the source as needing a fix.",[25,1964,1966],{"id":1965},"conclusion","Conclusion",[10,1968,1969],{},"To validate dates against a reporting period, read the column as text, parse each value with explicit formats including Excel serial numbers, and tag every row with the reasons it fails — not a date, implausible year, in the future, outside the period, non-working day, arrived as a number. Suggest a corrected date only where it lands inside the period — a mistyped year, or a day and month swap — and split the file into rows ready to load and rows for a person to fix.",[25,1971,1973],{"id":1972},"frequently-asked-questions","Frequently asked questions",[10,1975,1976,1979,1980,1983,1984,1987,1988,23],{},[1933,1977,1978],{},"How do I find dates outside a month in pandas?","\nCompare the parsed dates with ",[37,1981,1982],{},"period.start_time"," and ",[37,1985,1986],{},"period.end_time"," of a monthly ",[37,1989,1990],{},"Period",[10,1992,1993,1996,1997,23],{},[1933,1994,1995],{},"How do I convert an Excel serial number to a date in pandas?","\nUse ",[37,1998,1999],{},"pd.to_datetime(value, unit=\"D\", origin=\"1899-12-30\")",[10,2001,2002,2005],{},[1933,2003,2004],{},"How can I detect day and month swapped in dates?","\nFor day-first dates with a day of 12 or less, check whether swapping day and month moves the date into the expected period.",[10,2007,2008,2011],{},[1933,2009,2010],{},"Should invalid dates be corrected automatically?","\nNo; suggest corrections where the evidence is clear and let the person responsible accept them.",[25,2013,2015],{"id":2014},"related","Related",[1927,2017,2018,2024,2029,2036],{},[1930,2019,2020,2021,2023],{},"Up one level: ",[19,2022,22],{"href":21}," — checking data quality.",[1930,2025,2026,2028],{},[19,2027,1920],{"href":1919}," — the whole-file view.",[1930,2030,2031,2035],{},[19,2032,2034],{"href":2033},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fflag-public-holidays-in-excel-data-with-python\u002F","Flag Public Holidays in Excel Data with Python"," — the holiday calendar.",[1930,2037,2038,2042],{},[19,2039,2041],{"href":2040},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fbuild-a-data-quality-scorecard-for-an-excel-file\u002F","Build a Data Quality Scorecard for an Excel File"," — dates as one of many checks.",[2044,2045,2046],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":35,"searchDepth":93,"depth":93,"links":2048},[2049,2050,2051,2052,2053,2054,2055,2056,2057,2058],{"id":27,"depth":93,"text":28},{"id":66,"depth":93,"text":67},{"id":636,"depth":93,"text":637},{"id":1041,"depth":93,"text":1042},{"id":1662,"depth":93,"text":1663},{"id":1738,"depth":93,"text":1739},{"id":1924,"depth":93,"text":1925},{"id":1965,"depth":93,"text":1966},{"id":1972,"depth":93,"text":1973},{"id":2014,"depth":93,"text":2015},"Check the dates in a monthly Excel import before loading it: rows outside the reporting month, dates in the future, years with a typo such as 2062 or 1926, day and month swapped, numbers that are really Excel serials, text that is not a date at all, and transactions on days the business was closed — each row tagged with the reason and the likely correct date where one can be inferred.","md",[2062,2064,2066,2068],{"q":1978,"a":2063},"Compare the parsed dates with period.start_time and period.end_time of a monthly Period.",{"q":1995,"a":2065},"Use pd.to_datetime(value, unit=\"D\", origin=\"1899-12-30\").",{"q":2004,"a":2067},"For day-first dates with a day of 12 or less, check whether swapping day and month moves the date into the expected period.",{"q":2010,"a":2069},"No; suggest corrections where the evidence is clear and let the person responsible accept them.","validate dates within period pandas",null,{"breadcrumb":2073},[2074,2077,2080],{"name":2075,"item":2076},"Home","\u002F",{"name":2078,"item":2079},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":22,"item":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period",{"title":2083,"description":2084},"Validate Dates Fall Within an Expected Period with pandas","Validate Excel dates with pandas: out-of-period and future dates, year typos, day-month swaps, stray Excel serials, unparseable text and weekend or holiday transactions, each flagged with a reason and suggested fix.","validate-dates-fall-within-an-expected-period","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period\u002Findex",[2088,2089],"data-validation","dates-and-times","how-to","cQZS-H63TTRDdeemtKxXvilETKzSBr0fV8vfgE0dMdQ",[2093,2097],{"title":2094,"path":2095,"stem":2096,"children":-1},"Validate Check Digits in Excel Data with Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-check-digits-in-excel-data-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-check-digits-in-excel-data-with-python\u002Findex",{"title":2098,"path":2099,"stem":2100,"children":-1},"Validate Email and Phone Formats in Excel Data with Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data\u002Findex",[2102,2104],{"path":2103,"title":2078},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2105,"title":22},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python",[],[2108,2111,2114,2117,2120,2123],{"path":2109,"title":2110,"kind":2090},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas","Detect Outliers in Excel Data with pandas",{"path":2112,"title":2113,"kind":2090},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl","Highlight Cells Containing Specific Text with openpyxl",{"path":2115,"title":2116,"kind":2090},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffind-near-duplicate-names-within-one-excel-column","Find Near-Duplicate Names Within One Excel Column",{"path":2118,"title":2119,"kind":2090},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook","Read Dropdown Lists and Validation Rules from an Existing Workbook",{"path":2121,"title":2122,"kind":2090},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffind-and-report-missing-values-in-an-excel-file","Find and Report Missing Values in an Excel File",{"path":2124,"title":2125,"kind":2090},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Fflag-rows-with-too-many-missing-values","Flag Rows with Too Many Missing Values",1790972100855]