# 13.7 Text, dates and files (/building-ai-workers/programming-fundamentals/text-dates-files)

---
type: Document
title: "13.7 Text, dates and files"
description: "The hidden rules in text and dates: line endings, files, ISO dates, month and year ends, leap days and time zones."
status: stable
order: 313.07
ksor:
  owner: team:panaversity
  audience: [ public ]
  approval:
    by: process:panaversity
    at: 2026-11-02T09:10:15Z
chapter: "13"
part: III
expert_status: builder-only
concepts: [ "13.7" ]
last_verified: 2026-10-30
generated:
  at: 2026-11-02T09:10:15Z
  by: process:panaversity
trust_tier: unverified
build_id: sha256:e1fbcb7d9a8f8b9b7cb96875022325c02c9ba4be94cef13b301cbd39a9edad85
dirty: true
ksor_version: 0.0.60
---

**In everyday life.** At 9 p.m. on Monday in Columbus, it is already Tuesday morning in Tokyo. The same moment falls on a different date, depending on where you are. And a date written "10/11" may be October 11 or November 10, depending on the reader's country. Text and dates look simple, and both carry hidden rules.

**What you will learn.** Why a computer can get a date a day wrong, and which days to test on purpose, along with line endings and files.

**Why it matters.** Date code can pass on one computer and fail on the next.

### What goes wrong with text?

A **string** is text. Programs split it, trim it and compare it. Three things go wrong often:

- **Blank space and line endings.** Windows ends each line with two invisible characters, a carriage return `\r` and a newline `\n`. Mac and Linux use only `\n`. A TypeScript script that splits on `\n` alone leaves a `\r` stuck to the last value on each line, so `"open\r"` is not `"open"`. It is like a space at the end of a spreadsheet cell: the two look the same, but a lookup does not match them. [Lab 12](../agentic-coding/lab.md)'s brief warned the agent about this bug. TypeScript's `trim()` and Python's `strip()` remove blank space from the ends.
- **Commas inside values.** Brightline's exports have no commas inside values and no quoted fields, so splitting each line on commas works. Real exports often have a vendor name such as "Smith, Jones and Co.", in quotes, and splitting on commas cuts it in two. They need a CSV **parser**. A parser is code that reads the format properly. It comes from a library, ready-made code like a package. Ask the agent to use one for real data.
- **Case and spelling.** `"Net 30"` and `"net 30"` are different strings.

### How do I read a file?

Reading a file gives you its whole content as one string. The code then splits it into lines and values. When a file is missing, the program stops with an error that names the file's path, its address in the folders. [Concept 13.8](reading-an-error.md) shows how to read it. Brightline's scripts read a whole file at once, with TypeScript's `readFileSync` and Python's `Path.read_text`.

**Reading a file with `with`.** Agent-written Python often opens a file with a **`with` block** instead of reading it all at once. `with` opens the file and closes it when the block ends, even if an error stops the block. It is like a door that closes itself, whether you walk out or are pushed. Run it from the `brightline-ap` folder, because a short path such as `data/vendors.csv` is read from the folder the program runs in:

**In Python.**

```python
with open("data/vendors.csv", encoding="utf-8") as f:
    header = f.readline().strip()
print(header)
print(f.closed)
```

```text
vendor_id,vendor_name,terms
True
```

`readline()` reads one line. The second line of the output shows the file was closed after the block. [Chapter 14](../structuring-programs/reading-sdk-code.md) shows the same `with` shape closing a connection to an API.

![Figure 13.9. A with open(...) as f: block that reads a line from vendors.csv. Two paths leave the block. When it finishes, the file is closed and the program continues. When an error is raised, the file is closed and the error moves outward, where other code may catch it.](img/with-block.png)

*Figure 13.9. A with block. Whether the block ends normally or an error stops it, the file is closed.*

### Why are dates hard?

Brightline writes every date as **ISO** text: year, month, day, such as `2026-11-06`. ISO dates sort correctly as text, because each has the same length, zeros included, year first. That is why the [Chapter 12](../agentic-coding/lab.md) script can compare `due <= cutoff` directly. Date math is harder than it looks.

| Case | Example | Result |
| --- | --- | --- |
| Crossing a month end | `2026-10-20` plus 15 days | `2026-11-04` |
| Crossing a year end | `2026-12-20` plus 30 days | `2027-01-19` |
| A leap year | `2028-02-15` plus 15 days | `2028-03-01`, because February 2028 has 29 days |

### Why can a test pass on one computer and fail on another?

Because of time zones. A date, such as October 20, is the same everywhere. A moment is one instant, and it can fall on different dates in different places: 9 p.m. on Monday in Columbus is already Tuesday morning in Tokyo. In TypeScript, `Date` holds a moment, and some ways of making one use the computer's local time zone. So the same script can give a different day on a computer in New York and one in Tokyo. Brightline's code does date math in UTC, the world reference time, so the result is the same on every computer.

**At work.** The agent's first due-date function read the date with `new Date("2026-10-07")`, which is midnight UTC. Then it added the days and wrote the date back out in the computer's local time. Ana's tests passed on the team's build server, the shared computer that runs the tests, which is set to UTC. On her computer in Columbus they failed by one day. The agent had mixed local time and UTC, so the function was not pure: its answer depended on the computer. The fix was to build every date in UTC, and the lab runs the tests in three time zones to check it.

Here is that first function with Net 30 from October 7, on computers in three time zones. In October, midnight UTC on October 7 is still 8 p.m. on October 6 in Columbus.

| Computer's time zone | Due date | Why |
| --- | --- | --- |
| UTC | November 6 | The date stays October 7, so the answer is right |
| Columbus | November 5 | Four hours behind UTC, so the date starts as October 6 |
| Tokyo | November 6 | Nine hours ahead, so the date stays October 7 |

**In TypeScript.** `addDays` starts at midnight UTC on the date it is given, adds the days in UTC, and keeps the first 10 characters of the result: the year, month and day. The `Z` in `T00:00:00Z` means UTC. Its answers should match the table above.

```ts
function addDays(isoDate: string, days: number): string {
  const date = new Date(`${isoDate}T00:00:00Z`);
  date.setUTCDate(date.getUTCDate() + days);
  return date.toISOString().slice(0, 10);
}
console.log(addDays("2026-10-20", 15), addDays("2026-12-20", 30), addDays("2028-02-15", 15));
```

```text
2026-11-04 2027-01-19 2028-03-01
```

This next line depends on the computer's time zone. It was run on a computer set to New York time.

```ts
console.log(new Date(2026, 9, 20).toISOString());
```

```text
2026-10-20T04:00:00.000Z
```

`new Date(2026, 9, 20)` means midnight on October 20 in local time. The 9 means October, because months count from 0. New York is four hours behind UTC in October, so that moment is 4 a.m. in UTC. Tokyo is nine hours ahead, so on a computer there it prints 3 p.m. on the 19th.

Last, a line from a Windows file, with `\r` at its end. Predict both values.

```ts
const line = "INV-8301,V001,open\r";
console.log(line.split(",")[2] === "open", line.trim().split(",")[2] === "open");
```

```text
false true
```

**In Python.** Python's `date` is a calendar day with no time and no time zone, and a `timedelta` is a length of time, such as 15 days.

```python
from datetime import date, timedelta

def add_days(iso_date: str, days: int) -> str:
    return (date.fromisoformat(iso_date) + timedelta(days=days)).isoformat()

print(add_days("2026-10-20", 15), add_days("2026-12-20", 30), add_days("2028-02-15", 15))
line = "INV-8301,V001,open\r"
print(line.split(",")[2] == "open", line.strip().split(",")[2] == "open")
```

```text
2026-11-04 2027-01-19 2028-03-01
False True
```

**Where the languages differ.**

| | TypeScript | Python |
| --- | --- | --- |
| A calendar day | No such type. `Date` is a moment, so use UTC | `date`, with no time zone |
| `"2026-10-20"` read as a date | UTC midnight | The calendar day |
| Months in a date built from parts | Count from 0 (October is 9) | Count from 1 |
| Splitting lines | `split(/\r?\n/)` handles both endings | `splitlines()` handles both endings |

**The operating rule:** keep dates as ISO text, do date math in UTC or with a calendar-day type, and test month ends, year ends, leap days and Windows line endings on purpose.

![Figure 13.10. A calendar strip crossing a month end: October 20 plus 15 days lands on November 4, and February 15, 2028 plus 15 days lands on March 1 because of the leap day.](img/calendar-strip.png)

*Figure 13.10. Date math crosses edges. The edges are where the bugs are, so they are where the tests go.*

**Practice it.** [Practice: text, dates and files](both-ai-vendors.md#practice-text-dates-and-files), in your own tool, ten minutes.
