hanke580 opened a new issue, #54534:
URL: https://github.com/apache/arrow/issues/54534

   ### Describe the bug, including details regarding any error messages, 
version, and platform.
   
   
   `cast(timestamp[unit, tz] -> string)` formats a zoned timestamp as local 
wall time plus a `%z` offset. The wall time is computed with the zone's exact 
UTC offset, but `%z` prints only hours and minutes. When the offset has a 
seconds part, the text therefore names a different instant than the stored 
value, off by up to 59 seconds. Examples of such offsets: Africa/Monrovia until 
1972 (−0:44:30), Europe/Dublin until 1916, Europe/Paris until 1911, and the 
local mean time (LMT) of every IANA zone before it adopted standard time. 
`pyarrow.csv.write_csv` (and `write_dataset(format="csv")`) converts columns 
with this cast, so CSV exports of such values are wrong, and every reader of 
the file, pyarrow included, loads the shifted instant. No error or warning is 
raised.
   
   **Reproducer**
   
   ```python
   import io
   from datetime import datetime, timezone
   import pyarrow as pa
   import pyarrow.compute as pc
   import pyarrow.csv as csv
   
   # 1960-06-01 12:00:00 UTC; Africa/Monrovia was at UTC-00:44:30 then
   utc = pa.array([datetime(1960, 6, 1, 12, tzinfo=timezone.utc)], 
pa.timestamp("us", "UTC"))
   ts = utc.cast(pa.timestamp("us", "Africa/Monrovia"))
   print(ts[0].as_py())                        # 1960-06-01 11:15:30-00:44:30 
(correct)
   
   print(pc.cast(ts, pa.string())[0])          # cast to string
   
   buf = io.BytesIO()
   csv.write_csv(pa.table({"ts": ts}), buf)
   print(buf.getvalue().decode())
   
   back = csv.read_csv(io.BytesIO(buf.getvalue()),
                       convert_options=csv.ConvertOptions(column_types={"ts": 
pa.timestamp("us", "UTC")}))
   print(back["ts"][0], "!=", utc[0])
   ```
   
   Output on main:
   
   ```
   1960-06-01 11:15:30-00:44:30
   1960-06-01 11:15:30.000000-0044
   "ts"
   1960-06-01 11:15:30.000000-0044
   
   1960-06-01 11:59:30+00:00 != 1960-06-01 12:00:00+00:00
   ```
   
   `11:15:30` is the correct local time for offset −00:44:30, but it is printed 
next to `-0044`. Together they mean 11:59:30 UTC, 30 seconds off. DuckDB and 
Polars read the CSV as the same shifted instant. Other examples on main:
   - Europe/Dublin 1900-06-01 12:00 UTC → `11:34:39-0025` (−21 s)
   - Europe/Paris 1900-06-01 12:00 UTC → `12:09:21+0009` (+21 s)
   - America/New_York 1880-06-01 12:00 UTC → `07:03:58-0456` (−2 s)
   
   All units (s/ms/us/ns) are affected. Offsets that are whole minutes, and the 
`"UTC"` zone (formatted with `Z`), are correct.
   
   **Expected behavior:** the string names the same instant as the stored 
value. Two ways to get there:
   - Print the offset with its seconds (e.g. `-00:44:30`, as Python's 
`isoformat()` does).
   - Format in UTC (`...Z`) when the offset is not a whole number of minutes, 
or always for the CSV writer.
   
   **Versions:** pyarrow 27.0.0.dev13 (nightly built from main at 9f03413), 
Python 3.12, Linux x86_64 (Ubuntu 22.04). The released pyarrow 21.0.0 through 
26.0.0 wheels behave the same.
   
   **Where it comes from**
   
   - `CastFunctor<StringType, TimestampType>::ConvertZoned` formats with 
`"%Y-%m-%d %H:%M:%S%z"` for every zone other than `"UTC"` 
([scalar_cast_string.cc#L196-L215](https://github.com/apache/arrow/blob/9f0341314ca96eb58081d4132c77b460d880b2b0/cpp/src/arrow/compute/kernels/scalar_cast_string.cc#L196-L215)).
 It formats through `TimestampFormatter`, i.e. `chrono::to_stream` on a zoned 
time 
([temporal_internal.h#L167-L195](https://github.com/apache/arrow/blob/9f0341314ca96eb58081d4132c77b460d880b2b0/cpp/src/arrow/compute/kernels/temporal_internal.h#L167-L195)).
   - The vendored `date` library implements `%z` as 
`duration_cast<minutes>(*offset_sec)` and prints `±hhmm`, dropping the seconds 
of the offset 
([vendored/datetime/date.h#L6012-L6043](https://github.com/apache/arrow/blob/9f0341314ca96eb58081d4132c77b460d880b2b0/cpp/src/arrow/vendored/datetime/date.h#L6012-L6043)).
 The `%H:%M:%S` fields, by contrast, use the exact local time.
   - The CSV writer converts each column with `compute::Cast(data, utf8())` 
([csv/writer.cc#L131-L144](https://github.com/apache/arrow/blob/9f0341314ca96eb58081d4132c77b460d880b2b0/cpp/src/arrow/csv/writer.cc#L131-L144)),
 so it inherits the problem.
   
   **Impact:** historical, genealogical or astronomical timestamps stored in a 
local zone are exported to CSV (or cast to string) with instants that differ 
from the table by up to 59 seconds, without any error. Tables loaded back from 
such files hold the shifted values. Found by differential testing of CSV round 
trips.
   
   ### Component(s)
   
   C++, Python
   
   
   ### Component(s)
   
   C++


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to