{"course":{"slug":"agents-and-automation","title":"Agents and Automation","url":"https://addaly.com/learn/agents-and-automation","level":"intermediate","track":"build"},"lesson":{"slug":"dates-names-and-numbers","title":"The boring bugs that eat automations","url":"https://addaly.com/learn/agents-and-automation/dates-names-and-numbers","position":24,"of":72,"minutes":9,"answer":"Money in floating point, a date without a locale and a name split into first and last are the three defects that survive every review and corrupt data quietly for months.","keyIdea":"Money in floating point, a date without a locale and a name split into first and last are the three defects that survive every review and corrupt data quietly for months."},"body":{"format":"markdown","text":"## Money is not a float\n\n`0.1 + 0.2` is `0.30000000000000004` in Python, JavaScript, Java and every other language using binary floating point, because a decimal fraction like 0.1 has no exact binary representation, the same way a third has no exact decimal one. The error is tiny and it accumulates.\n\nOver ten thousand invoice lines your total drifts from the accounting system's by a few pence, reconciliation fails, and somebody spends two days looking for a missing transaction that does not exist.\n\nStore money as an integer number of minor units (paise, cents, pence) or as a decimal type:\n\n```python\nfrom decimal import Decimal\ntotal = Decimal(\"48200.00\") + Decimal(\"0.10\")   # exact\n```\n\nParse from the string, never through a float on the way. `Decimal(0.1)` inherits the error that `Decimal(\"0.1\")` avoids. And keep the currency next to the amount in the same object, because an amount without a currency is a number waiting to be added to the wrong thing.\n\n## A date is not a date without a locale\n\n`03/04/2026` is the third of April in Mumbai and London, and the fourth of March in Chicago. The string parses successfully under both readings, which is what makes it dangerous: there is no error to catch, only a value that is wrong by a month.\n\nRules that work:\n\n- Internally, one format only: ISO 8601, `2026-04-03`. Unambiguous and sorts correctly as text.\n- When a source is ambiguous, you must know the locale by other means. If you do not, refuse rather than guess, and route it to a person. Guessing here is the definition of an invisible error.\n- Store timestamps in UTC and keep the original offset alongside. \"Which day did this happen on\" has a different answer in Auckland and Los Angeles for the same instant.\n- Daily jobs and clock changes: a job scheduled at 02:30 local time does not run at all on the spring-forward day in countries that observe daylight saving, and runs twice in autumn. Schedule in UTC, or pick a time that does not exist in the danger window, and make the job idempotent so a double run is harmless.\n\n## Number formats differ by more than currency symbols\n\n`1.234,56` is one thousand two hundred and thirty-four in most of Europe and something else entirely if parsed as English. Indian grouping puts `12,34,567` where an English reader expects `1,234,567`. Neither breaks a naive comma-strip, but abbreviations do: `12L`, `1.2 Cr`, `₹4.5k` all appear in real business documents and none of them parse.\n\nDetect and normalise explicitly, keep the raw string, and reject what you cannot parse rather than defaulting to zero. Silent zeros are how a report shows a department spending nothing.\n\n## Names do not have parts\n\nThere is no reliable split into first and last. Order varies by culture, many people have one name, many have four, particles like *van der* attach in ways that regex does not capture, and transliteration produces Mohammed, Muhammad, Mohd and Md for the same person.\n\nSo: store the name as given, in one field, exactly as the person wrote it. If you need a matching key, compute one (lowercased, unaccented, whitespace-collapsed) and store it *beside* the original, never over it. A normalised key is a tool for your code. The name is the person's, and an automation that corrects somebody's name to fit a schema is doing damage that is visible to them and invisible to you.\n\n## Encoding, and the CSV that starts with a ghost\n\nA UTF-8 file saved by Excel often begins with a byte-order mark. Read it without accounting for that and your first column is named `\\ufeffinvoice_id` instead of `invoice_id`, every lookup on that column returns nothing, and the header looks completely normal in every editor you inspect it with. In Python, open CSVs with `encoding=\"utf-8-sig\"` and the problem disappears.\n\nThe related symptom is mojibake (`â€™` where an apostrophe should be) which is UTF-8 bytes being read as Windows-1252. Fix it at the read, never with a find-and-replace downstream, because you will only ever patch the characters you happened to notice.\n\n## Units, and the general rule\n\nKilograms and pounds, metres and feet, per-item and per-case pricing. Put the unit in the column name or in a companion column, and refuse arithmetic between different units at the type level if your language lets you.\n\nUnderneath all of these is one rule: **parse at the edge, normalise once, and keep the raw value.** The moment you have both the printed string and your parsed value stored side by side, every one of the bugs above becomes a query you can run rather than an archaeology project."},"check":{"question":"An automation stores invoice totals as floating-point numbers. After some months, its monthly total differs from the accounting system’s by a few pence and the difference is never the same twice. What is happening?","options":[{"text":"Rounding is applied at display time but not at storage, so the stored values disagree with what people read.","correct":false,"tempting":"Display rounding causes discrepancies of a consistent, explainable kind, not a drift that changes size with the number of rows."},{"text":"Values arriving in different currencies are being summed without conversion.","correct":false,"tempting":"A currency mix-up produces large, obvious discrepancies rather than a few pence: the size of the error is the clue here."},{"text":"Decimal fractions have no exact binary representation, so each stored amount carries a tiny error that accumulates across thousands of rows.","correct":true},{"text":"The database column has too few decimal places, truncating each value slightly.","correct":false,"tempting":"Truncation biases consistently in one direction and would be reproducible; the moving, sign-varying drift points at representation, not precision settings."}]},"prev":{"slug":"extraction-that-holds-up","title":"Turning a document into a row you can defend","url":"https://addaly.com/learn/agents-and-automation/extraction-that-holds-up"},"next":{"slug":"deduplication-and-matching","title":"The same customer, three times","url":"https://addaly.com/learn/agents-and-automation/deduplication-and-matching"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The boring bugs that eat automations', in Agents and Automation. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/agents-and-automation/dates-names-and-numbers (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The boring bugs that eat automations","courseTitle":"Agents and Automation","module":{"index":3,"slug":"data-plumbing","title":"The data underneath: sources, documents, records"},"answer":null,"keyIdea":"Money in floating point, a date without a locale and a name split into first and last are the three defects that survive every review and corrupt data quietly for months.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"money-is-not-a-float","runs":[{"text":"Money is not a float"}]},{"t":"p","runs":[{"text":"0.1 + 0.2","code":true},{"text":" is "},{"text":"0.30000000000000004","code":true},{"text":" in Python, JavaScript, Java and every other language using binary floating point, because a decimal fraction like 0.1 has no exact binary representation, the same way a third has no exact decimal one. The error is tiny and it accumulates."}]},{"t":"p","runs":[{"text":"Over ten thousand invoice lines your total drifts from the accounting system’s by a few pence, reconciliation fails, and somebody spends two days looking for a missing transaction that does not exist."}]},{"t":"p","runs":[{"text":"Store money as an integer number of minor units (paise, cents, pence) or as a decimal type:"}]},{"t":"code","lang":"python","label":"Python","text":"from decimal import Decimal\ntotal = Decimal(\"48200.00\") + Decimal(\"0.10\")   # exact"},{"t":"p","runs":[{"text":"Parse from the string, never through a float on the way. "},{"text":"Decimal(0.1)","code":true},{"text":" inherits the error that "},{"text":"Decimal(\"0.1\")","code":true},{"text":" avoids. And keep the currency next to the amount in the same object, because an amount without a currency is a number waiting to be added to the wrong thing."}]},{"t":"h2","id":"a-date-is-not-a-date-without-a-locale","runs":[{"text":"A date is not a date without a locale"}]},{"t":"p","runs":[{"text":"03/04/2026","code":true},{"text":" is the third of April in Mumbai and London, and the fourth of March in Chicago. The string parses successfully under both readings, which is what makes it dangerous: there is no error to catch, only a value that is wrong by a month."}]},{"t":"p","runs":[{"text":"Rules that work:"}]},{"t":"ul","items":[[{"text":"Internally, one format only: ISO 8601, "},{"text":"2026-04-03","code":true},{"text":". Unambiguous and sorts correctly as text."}],[{"text":"When a source is ambiguous, you must know the locale by other means. If you do not, refuse rather than guess, and route it to a person. Guessing here is the definition of an invisible error."}],[{"text":"Store timestamps in UTC and keep the original offset alongside. “Which day did this happen on” has a different answer in Auckland and Los Angeles for the same instant."}],[{"text":"Daily jobs and clock changes: a job scheduled at 02:30 local time does not run at all on the spring-forward day in countries that observe daylight saving, and runs twice in autumn. Schedule in UTC, or pick a time that does not exist in the danger window, and make the job idempotent so a double run is harmless."}]]},{"t":"h2","id":"number-formats-differ-by-more-than-currency-symbols","runs":[{"text":"Number formats differ by more than currency symbols"}]},{"t":"p","runs":[{"text":"1.234,56","code":true},{"text":" is one thousand two hundred and thirty-four in most of Europe and something else entirely if parsed as English. Indian grouping puts "},{"text":"12,34,567","code":true},{"text":" where an English reader expects "},{"text":"1,234,567","code":true},{"text":". Neither breaks a naive comma-strip, but abbreviations do: "},{"text":"12L","code":true},{"text":", "},{"text":"1.2 Cr","code":true},{"text":", "},{"text":"₹4.5k","code":true},{"text":" all appear in real business documents and none of them parse."}]},{"t":"p","runs":[{"text":"Detect and normalise explicitly, keep the raw string, and reject what you cannot parse rather than defaulting to zero. Silent zeros are how a report shows a department spending nothing."}]},{"t":"h2","id":"names-do-not-have-parts","runs":[{"text":"Names do not have parts"}]},{"t":"p","runs":[{"text":"There is no reliable split into first and last. Order varies by culture, many people have one name, many have four, particles like "},{"text":"van der","i":true},{"text":" attach in ways that regex does not capture, and transliteration produces Mohammed, Muhammad, Mohd and Md for the same person."}]},{"t":"p","runs":[{"text":"So: store the name as given, in one field, exactly as the person wrote it. If you need a matching key, compute one (lowercased, unaccented, whitespace-collapsed) and store it "},{"text":"beside","i":true},{"text":" the original, never over it. A normalised key is a tool for your code. The name is the person’s, and an automation that corrects somebody’s name to fit a schema is doing damage that is visible to them and invisible to you."}]},{"t":"h2","id":"encoding-and-the-csv-that-starts-with-a-ghost","runs":[{"text":"Encoding, and the CSV that starts with a ghost"}]},{"t":"p","runs":[{"text":"A UTF-8 file saved by Excel often begins with a byte-order mark. Read it without accounting for that and your first column is named "},{"text":"\\ufeffinvoice_id","code":true},{"text":" instead of "},{"text":"invoice_id","code":true},{"text":", every lookup on that column returns nothing, and the header looks completely normal in every editor you inspect it with. In Python, open CSVs with "},{"text":"encoding=\"utf-8-sig\"","code":true},{"text":" and the problem disappears."}]},{"t":"p","runs":[{"text":"The related symptom is mojibake ("},{"text":"â€™","code":true},{"text":" where an apostrophe should be) which is UTF-8 bytes being read as Windows-1252. Fix it at the read, never with a find-and-replace downstream, because you will only ever patch the characters you happened to notice."}]},{"t":"h2","id":"units-and-the-general-rule","runs":[{"text":"Units, and the general rule"}]},{"t":"p","runs":[{"text":"Kilograms and pounds, metres and feet, per-item and per-case pricing. Put the unit in the column name or in a companion column, and refuse arithmetic between different units at the type level if your language lets you."}]},{"t":"p","runs":[{"text":"Underneath all of these is one rule: "},{"text":"parse at the edge, normalise once, and keep the raw value.","b":true},{"text":" The moment you have both the printed string and your parsed value stored side by side, every one of the bugs above becomes a query you can run rather than an archaeology project."}]}],"check":{"question":[{"text":"An automation stores invoice totals as floating-point numbers. After some months, its monthly total differs from the accounting system’s by a few pence and the difference is never the same twice. What is happening?"}],"options":[{"text":[{"text":"Rounding is applied at display time but not at storage, so the stored values disagree with what people read."}],"correct":false,"tempting":[{"text":"Display rounding causes discrepancies of a consistent, explainable kind, not a drift that changes size with the number of rows."}]},{"text":[{"text":"Values arriving in different currencies are being summed without conversion."}],"correct":false,"tempting":[{"text":"A currency mix-up produces large, obvious discrepancies rather than a few pence: the size of the error is the clue here."}]},{"text":[{"text":"Decimal fractions have no exact binary representation, so each stored amount carries a tiny error that accumulates across thousands of rows."}],"correct":true,"tempting":null},{"text":[{"text":"The database column has too few decimal places, truncating each value slightly."}],"correct":false,"tempting":[{"text":"Truncation biases consistently in one direction and would be reproducible; the moving, sign-varying drift points at representation, not precision settings."}]}],"order":[1,3,0,2],"why":null},"prev":{"slug":"extraction-that-holds-up","title":"Turning a document into a row you can defend","minutes":10},"next":{"slug":"deduplication-and-matching","title":"The same customer, three times","minutes":9}}}