iostreams 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/docs/config.md +13 -9
  3. data/docs/copy_files.md +20 -2
  4. data/docs/extensions.md +18 -4
  5. data/docs/formats.md +644 -16
  6. data/docs/path.md +18 -2
  7. data/docs/streams.md +54 -9
  8. data/docs/tutorial.md +14 -2
  9. data/docs/upgrading.md +470 -23
  10. data/lib/io_streams/builder/deprecations.rb +250 -0
  11. data/lib/io_streams/builder.rb +256 -39
  12. data/lib/io_streams/bzip2/reader.rb +18 -2
  13. data/lib/io_streams/bzip2/writer.rb +11 -2
  14. data/lib/io_streams/deprecation/methods.rb +51 -0
  15. data/lib/io_streams/deprecation/once.rb +27 -0
  16. data/lib/io_streams/deprecation/option_names.rb +26 -0
  17. data/lib/io_streams/deprecation/words.rb +42 -0
  18. data/lib/io_streams/deprecation.rb +259 -0
  19. data/lib/io_streams/encode/reader.rb +54 -3
  20. data/lib/io_streams/encode/writer.rb +19 -2
  21. data/lib/io_streams/encode.rb +99 -0
  22. data/lib/io_streams/errors.rb +51 -0
  23. data/lib/io_streams/gzip/reader.rb +11 -0
  24. data/lib/io_streams/io_streams.rb +365 -35
  25. data/lib/io_streams/line/reader.rb +2 -1
  26. data/lib/io_streams/line/utf8_reader.rb +23 -0
  27. data/lib/io_streams/path.rb +125 -35
  28. data/lib/io_streams/paths/file.rb +72 -9
  29. data/lib/io_streams/paths/http.rb +48 -2
  30. data/lib/io_streams/paths/matcher.rb +6 -3
  31. data/lib/io_streams/paths/s3.rb +180 -9
  32. data/lib/io_streams/paths/sftp.rb +47 -3
  33. data/lib/io_streams/paths.rb +23 -0
  34. data/lib/io_streams/pgp/reader.rb +18 -0
  35. data/lib/io_streams/pgp/writer.rb +16 -0
  36. data/lib/io_streams/pgp.rb +16 -2
  37. data/lib/io_streams/reader.rb +44 -0
  38. data/lib/io_streams/record/reader.rb +59 -68
  39. data/lib/io_streams/record/writer.rb +17 -10
  40. data/lib/io_streams/row/header_row.rb +21 -0
  41. data/lib/io_streams/row/reader.rb +52 -15
  42. data/lib/io_streams/row/writer.rb +64 -13
  43. data/lib/io_streams/schema/columns.rb +456 -0
  44. data/lib/io_streams/schema/constraints.rb +99 -0
  45. data/lib/io_streams/schema/field.rb +967 -0
  46. data/lib/io_streams/schema/json_number.rb +56 -0
  47. data/lib/io_streams/schema/keys.rb +78 -0
  48. data/lib/io_streams/schema/pattern.rb +257 -0
  49. data/lib/io_streams/schema/source.rb +31 -0
  50. data/lib/io_streams/schema.rb +616 -0
  51. data/lib/io_streams/stream.rb +836 -67
  52. data/lib/io_streams/symmetric_encryption/reader.rb +8 -0
  53. data/lib/io_streams/tabular/header.rb +29 -0
  54. data/lib/io_streams/tabular/header_mapping.rb +332 -0
  55. data/lib/io_streams/tabular/mapping.rb +165 -0
  56. data/lib/io_streams/tabular/parser/array.rb +5 -0
  57. data/lib/io_streams/tabular/parser/base.rb +24 -0
  58. data/lib/io_streams/tabular/parser/fixed.rb +197 -0
  59. data/lib/io_streams/tabular/parser/fixed_fields.rb +160 -0
  60. data/lib/io_streams/tabular/parser/hash.rb +10 -0
  61. data/lib/io_streams/tabular/parser/json.rb +17 -0
  62. data/lib/io_streams/tabular/parser/json_fields.rb +132 -0
  63. data/lib/io_streams/tabular/schema_mapping.rb +361 -0
  64. data/lib/io_streams/tabular.rb +344 -30
  65. data/lib/io_streams/utils.rb +441 -6
  66. data/lib/io_streams/version.rb +1 -1
  67. data/lib/io_streams/writer.rb +27 -0
  68. data/lib/io_streams/xlsx/cells.rb +127 -0
  69. data/lib/io_streams/xlsx/reader.rb +56 -0
  70. data/lib/io_streams/xlsx/schema_reader.rb +90 -0
  71. data/lib/io_streams/xlsx/v3_writer.rb +16 -0
  72. data/lib/io_streams/zip/reader.rb +7 -0
  73. data/lib/io_streams/zip/writer.rb +24 -2
  74. data/lib/iostreams.rb +21 -17
  75. metadata +28 -2
data/docs/formats.md CHANGED
@@ -3,7 +3,7 @@ layout: default
3
3
  title: File Formats
4
4
  description: >-
5
5
  Converting rows and records to and from CSV, PSV, JSON and fixed width files,
6
- including format inference, format options and header handling.
6
+ including format inference, format options, header handling and schemas.
7
7
  ---
8
8
 
9
9
  When reading or writing rows (`:array`) or records (`:hash`), IOStreams converts each line
@@ -19,6 +19,9 @@ to or from the file's tabular format. The following formats are supported:
19
19
  PSV has no way to escape values, so when writing PSV, a `|` within a value is replaced with `:`
20
20
  and a line break with a space, so that a value cannot add columns or records.
21
21
 
22
+ Each value is read as text, unless a [schema](#schema) gives the type of its field, such as an
23
+ `Integer` or a `Date`.
24
+
22
25
  ## Format inference
23
26
 
24
27
  The format is inferred from the file name when it contains a recognized extension:
@@ -50,16 +53,32 @@ path.each(:hash) { |record| p record }
50
53
  IOStreams.path("sample_data").format(:json).each(:hash) { |record| p record }
51
54
  ~~~
52
55
 
56
+ `IOStreams.tabular_formats` returns the names of the formats that `format` accepts, such as `:csv`, `:json` and
57
+ `:fixed`, for example to check a format held in configuration:
58
+
59
+ ~~~ruby
60
+ IOStreams.tabular_formats.include?(:json)
61
+ # => true
62
+ ~~~
63
+
53
64
  ## Format options
54
65
 
55
66
  Format specific options are supplied with `format_options`. They are passed to the parser
56
- for the chosen format. The `:fixed` format requires its file layout to be supplied this way,
57
- as shown in the next section. The other formats do not currently take any options.
67
+ for the chosen format. Only the `:fixed` format takes any: the layout of its file, as shown in
68
+ the next section.
69
+
70
+ `format_options` is deprecated, and reports a deprecation warning, see
71
+ [Seeing deprecation warnings](upgrading#seeing-deprecation-warnings): give the `width` of each field
72
+ of a fixed width file in a [schema](#fixed-width-files-with-a-schema) instead, see
73
+ [Moving a layout to a schema](upgrading#moving-a-layout-to-a-schema). It still works as before.
58
74
 
59
75
  ## Fixed width files
60
76
 
61
77
  Fixed width files have no delimiters; each column is identified by its position within the line.
62
- Since the layout cannot be inferred from the file, supply it using `format_options`:
78
+ Since the layout cannot be inferred from the file, give the `width` of each field in a
79
+ [schema](#fixed-width-files-with-a-schema), which also gives the type of each field. The layout of the
80
+ deprecated `format_options`, below, still works as before, see
81
+ [Moving a layout to a schema](upgrading#moving-a-layout-to-a-schema):
63
82
 
64
83
  ~~~ruby
65
84
  path = IOStreams.path("sample_data")
@@ -94,6 +113,11 @@ end
94
113
  Note: The keys in the hashes being written must match the layout `:key` values exactly,
95
114
  including whether they are strings or symbols.
96
115
 
116
+ A layout does not take the columns of the rows written from a header row, such as the `IOStreams::Row::HeaderRow`
117
+ that `each(:array)` yields with a schema, or that `copy_from(source, mode: :array)` hands it from a source with a
118
+ schema, since it does not match them to its keys, so it raises `IOStreams::Errors::InvalidHeader`, rather than write
119
+ blank values. Give the target a [schema](#fixed-width-files-with-a-schema) instead, which matches them to its fields.
120
+
97
121
  Layout column definitions:
98
122
 
99
123
  * `:size` The number of characters this column occupies.
@@ -119,7 +143,17 @@ In addition to `layout`, the `:fixed` format takes one more option:
119
143
 
120
144
  ## Header options
121
145
 
122
- When reading or writing records (`:hash`), the following options control the header row:
146
+ When reading or writing records (`:hash`), the following options control the header row.
147
+ They are deprecated, and report a deprecation warning, as does `cleanse_header`, even when they
148
+ are nil: a [schema](#schema) describes the columns of the file on its path instead, see
149
+ [Moving the column options to a schema](upgrading#moving-the-column-options-to-a-schema). They still work
150
+ as before without a schema. A writer ignores `allowed_columns`, `required_columns` and `skip_unknown`, which
151
+ only apply when reading.
152
+
153
+ With a [schema](#schema), which describes the columns of the file instead, `columns`,
154
+ `allowed_columns` and `required_columns` raise `ArgumentError` unless they are nil, and so
155
+ does `cleanse_header`, even when it is nil. `skip_unknown` only applies to `allowed_columns`,
156
+ so it is accepted. A nil one, and `skip_unknown`, have no effect.
123
157
 
124
158
  * `columns: [Array<String>]`
125
159
  When reading, supplies the header columns for files that do not include a header row.
@@ -132,6 +166,11 @@ When reading or writing records (`:hash`), the following options control the hea
132
166
  and dashes are converted to underscores, so the header `" First Name "` becomes `"first_name"`.
133
167
  Default: true
134
168
 
169
+ Without a schema, two columns of a header row that are the same key of a record, such as `a,a,b`, or `A` and `a`,
170
+ which are cleansed to the same key, give each record the value of only the last of them. This is deprecated, and
171
+ reports a deprecation warning, since IOStreams 3.0 raises `IOStreams::Errors::InvalidHeader` for them, rather than
172
+ lose the others: rename the columns, or read the rows with `each(:array)`, which holds every column.
173
+
135
174
  * `allowed_columns: [Array<String>]`
136
175
  List of columns to allow. Any other columns are ignored when `skip_unknown` is true,
137
176
  otherwise an `IOStreams::Errors::InvalidHeader` exception is raised.
@@ -152,17 +191,10 @@ for JSON and `:hash` input, when `columns` are supplied, and with `cleanse_heade
152
191
  warning is logged when applying them would change the records read.
153
192
 
154
193
  Since the format is usually inferred from the file name, renaming an uploaded file from `.csv` to
155
- `.json` bypasses them. When they restrict which columns an upload can set, apply them to every input
156
- in an initializer:
157
-
158
- ~~~ruby
159
- IOStreams.enforce_column_restrictions = true
160
- ~~~
161
-
162
- They then also apply to the supplied `columns`, to a header row read with `cleanse_header: false`,
163
- and, for formats without a header row such as JSON, to the keys of each record. When either
164
- `allowed_columns` or `required_columns` is set, JSON keys are cleansed the same way as a header row,
165
- unless `cleanse_header: false` is supplied. This will be the default in v3.0.
194
+ `.json` bypasses them. A [schema](#schema) applies to every input instead, including the keys of each
195
+ JSON record. `IOStreams.enforce_column_restrictions = true` applies the options to every input too,
196
+ but it is deprecated with them, see
197
+ [Moving the column options to a schema](upgrading#moving-the-column-options-to-a-schema).
166
198
 
167
199
  Example, reading a headerless CSV file:
168
200
 
@@ -186,3 +218,599 @@ path.read
186
218
 
187
219
  Note: Column names are converted to strings, and the keys in the hashes being written may
188
220
  be strings or symbols.
221
+
222
+ ## Schema
223
+
224
+ A schema gives the fields of the rows and records of a file, and the type of each one, so that reading them returns
225
+ each value as its type, such as an `Integer` or a `Date`, rather than as text, and writing them writes each value in
226
+ the text form of its type. It applies to every format, and gives the position of each field of a fixed width file.
227
+ Its fields name the keys of the records read, and say which columns a file can have and must have, so that the keys
228
+ do not depend on how cleansing changes a column, such as `Amount ($)` and `Amount (€)`, which both cleanse to
229
+ `amount_`.
230
+
231
+ A schema is a [Frictionless Table Schema](https://datapackage.org/standard/table-schema/), set on the path with
232
+ `#schema`, either as a Hash:
233
+
234
+ ~~~ruby
235
+ # Read the id and joined columns of the file as their types, and skip its other columns.
236
+ path = IOStreams.path("people.csv").schema(
237
+ fieldsMatch: "partial",
238
+ fields: [
239
+ {name: "id", type: "integer"},
240
+ {name: "joined", type: "date"}
241
+ ]
242
+ )
243
+ path.each(:hash) do |person|
244
+ p person
245
+ end
246
+ # => {"id" => 42, "joined" => #<Date: 2026-10-13 ((2461327j,0s,0n),+0s,2299161j)>}
247
+ ~~~
248
+
249
+ Or as the path or url of a JSON file that holds it, in any store that IOStreams reads, so that the schema can be held
250
+ in configuration, like the path of the file:
251
+
252
+ ~~~ruby
253
+ IOStreams.path(ENV["PEOPLE_PATH"]).schema(ENV["PEOPLE_SCHEMA"]).each(:hash) do |person|
254
+ p person
255
+ end
256
+ ~~~
257
+
258
+ The columns of the file are matched to the fields, and the keys of each record read are the names of its fields,
259
+ see [Matching columns to fields](#matching-columns-to-fields). `fieldsMatch: "partial"` above reads the fields that
260
+ the file has, and skips its other columns. Without `fieldsMatch`, as in Frictionless, the columns of the file must be
261
+ the fields, in order, which is what a file written with the schema has:
262
+
263
+ ~~~ruby
264
+ path = IOStreams.path("people.csv").schema(
265
+ fields: [
266
+ {name: "id", type: "integer"},
267
+ {name: "pay", type: "number", decimals: 2},
268
+ {name: "joined", type: "date"},
269
+ {name: "active", type: "boolean"}
270
+ ]
271
+ )
272
+ path.writer(:hash) do |io|
273
+ io << {"id" => 42, "pay" => BigDecimal("12.5"), "joined" => Date.new(2026, 10, 13), "active" => true}
274
+ end
275
+ path.read
276
+ # => "id,pay,joined,active\n42,12.50,2026-10-13,true\n"
277
+
278
+ path.each(:hash) do |person|
279
+ p person
280
+ end
281
+ # => {"id" => 42, "pay" => 0.125e2, "joined" => #<Date: 2026-10-13 ((2461327j,0s,0n),+0s,2299161j)>, "active" => true}
282
+ ~~~
283
+
284
+ A schema in a Hash is checked when it is set. A schema file is read the first time that a reader or writer needs it,
285
+ so that setting it does not read the file, and the copies of a path read it once. `#schema` returns the schema
286
+ that was set. Without a schema, the rows and records of a file are read and written as before.
287
+
288
+ Like `#format`, the schema describes the file, so it is set on the path, rather than supplied to each call of
289
+ `#each`, `#reader` or `#writer`, which raises `ArgumentError` for `schema:`. Since it describes the columns of the
290
+ file, the [header options](#header-options) that describe them without a schema raise `ArgumentError` with it, and
291
+ so does the layout of a fixed width file in `#format_options`, when a reader or writer of the path is created.
292
+
293
+ The text of a file read with a schema is UTF-8, so that its values are UTF-8 strings, and so are its lines read with
294
+ `each(:line)`, and text that is not valid UTF-8 raises `IOStreams::Errors::InvalidEncoding`, which is an
295
+ `Encoding::UndefinedConversionError`, naming the line that it is on, which its `#line_number` returns. An encoding set
296
+ with `encoding`, such as `encoding("BINARY")`, is used instead, without checking the text, as it is without a schema,
297
+ and `raw` reads the data as it is stored. Binary text written to rows and records with a schema, such as the values of
298
+ a file without one that is copied, is written as UTF-8, and raises `IOStreams::Errors::InvalidEncoding` when it is not
299
+ valid UTF-8, unless an encoding is set, which writes it instead, as it does without a schema. Lines written with
300
+ `writer(:line)` are written as they are.
301
+
302
+ ### Types
303
+
304
+ | Type | Read as | Written as |
305
+ |------|---------|------------|
306
+ | `string` | a `String`, such as the number of a cell of a spreadsheet, `2134` rather than `2134.0`, see below | the text, or the text of any other value, except a `Hash` or an `Array`, which raise |
307
+ | `integer` | an `Integer`, from text such as `42`, `-12` or `0042`, or a whole number with a decimal point, such as `12.0`, as a spreadsheet stores it | `42` |
308
+ | `number` | a `BigDecimal`, from text such as `12.5`, `-1.5E3`, `NaN`, `INF` or `-INF`, or `Infinity` or `-Infinity`, as Ruby and JavaScript write them, and frictionless-py also reads them, whatever their case, or a number, including a `Rational`, or a `Float` with `floatNumber: true` | `12.5`, a whole number without a decimal point, such as `42` for `42`, `42.0` or the text `42.0`, or `12.50` and `42.00` with `decimals: 2`, and a number that is not finite as `NaN`, `INF` or `-INF`, except to JSON, which cannot hold it, so that it raises `IOStreams::Errors::InvalidValue` |
309
+ | `boolean` | `true` or `false`, from one of its `trueValues` or `falseValues`, or the number `1` or `0`, such as a number of JSON, also `1.0` or `0.0`, while any other number raises | the first of its `trueValues` or `falseValues` |
310
+ | `date` | a `Date`, from `2026-10-13`, or text in its `format`, or the date of a `Time` or `DateTime`, which is deprecated for one that is not at midnight, since v3.0 raises for it, see [Deprecated, and what replaces it](upgrading#deprecated-and-what-replaces-it) | `2026-10-13`, or text in its `format` |
311
+ | `datetime` | a `Time`, from ISO 8601 with seconds, such as `2026-10-13T09:30:00Z`, also with a space in place of the `T`, such as `2026-10-13 09:30:00`, and with an offset from UTC without a colon, such as `+0530`, or of hours alone, such as `+05`, or text in its `format`. Text without an offset from UTC is read as UTC, as is `Z`, while an offset of zero that the text says, such as `+00:00`, or `+0000` with a `format`, is kept. The hour `24`, which ISO 8601 allows for the end of a day, such as `2026-10-13T24:00:00`, raises | ISO 8601, such as `2026-10-13T09:30:00+05:30`, or `2026-10-13T09:30:00Z` for a time in UTC, also a `DateTime` at an offset of zero, which does not tell UTC from another zone at that offset, or text in its `format` |
312
+ | `object` | a `Hash`, from the text of a JSON object, such as `{"a":1}`, or a `Hash` as JSON holds it, see below | that JSON text, or in JSON an object |
313
+ | `array` | an `Array`, from the text of a JSON array, such as `[1,2]`, or an `Array` as JSON holds it, see below | that JSON text, or in JSON an array |
314
+ | `any` | the value as it is read | the value, with a `BigDecimal`, `Date` or `Time` as its text, and in a format of text a `Hash` or an `Array` as JSON, see [Values of a field without a type](#values-of-a-field-without-a-type) |
315
+
316
+ A field without a `type` is `any`, as in Frictionless.
317
+
318
+ Empty text is a missing value, read as `nil` in every type, including a `string` or `any` field, as the default
319
+ `missingValues` of Frictionless, `[""]`, says, so that a missing value is `nil` whether the file leaves it out, such
320
+ as JSON's `null`, or holds empty text, such as `""` in CSV or JSON. Text of only whitespace is not missing: a `string`
321
+ or `any` field keeps it as the text, and any other type raises `IOStreams::Errors::InvalidValue` for it, as it does
322
+ for `N/A`. The leading and trailing whitespace of any other text is removed before it is read as a type other than
323
+ `string` or `any`, so that ` 42 ` is the `integer` `42`. Writing works the same way, so that empty text is written
324
+ as a missing value, which CSV writes as an empty value, such as `,21,`, rather than a quoted `""`, and JSON as
325
+ `null`, and text of only whitespace raises for a type other than `string` or `any`.
326
+
327
+ A spreadsheet read with a schema holds the number of each number cell, to the 15 significant digits that Excel
328
+ displays, or every digit of a whole number below 2^53, which a `Float` holds exactly, such as the card number
329
+ `4111111111111111`, without its number format, such as `0.125` for `12.5%`, in plain digits, such as `0.0000001`, and
330
+ without a decimal point for a whole number, such as `2134`, or `0.3` for `0.30000000000000004`, in its lines and in
331
+ its rows and records, so that a `string` field reads that text, and a `number` field those digits. A cell of a date
332
+ holds its date in ISO 8601, such as `2026-10-25`, one of a date and time the time of its clock in UTC, such as
333
+ `2026-10-25T09:30:00Z`, since Excel has no time zone, and one of a time of day the time of its clock, such as
334
+ `09:30:00`, so that a `date` or `datetime` field reads them. Which cells are dates and times is decided by their
335
+ number formats, as v3.0 decides it: Excel's own formats of a date, a time of day, or both, and a custom format of
336
+ only a year, month or day, such as `yyyy-mm-dd` or `dd/mm/yyyy`, which is a date, of only hours, minutes or seconds,
337
+ such as `hh:mm:ss`, a time of day, or of both, such as `dd/mm/yyyy hh:mm`, a date and time, while quoted text, such
338
+ as the `d` of `0.00" days"`, or a color, such as `[Red]0.00`, does not make a number a date. Without a schema, its
339
+ cells are read as before, such as `2134.0`, as creek reads them, which takes every custom format with a letter of a
340
+ date or a time, such as `yyyy-mm-dd` or `0.00" days"`, as a date and time.
341
+
342
+ A `Hash` or an `Array` of an `object` or `array` field, such as one of the `:hash` format, is converted to what JSON
343
+ holds of it, so that it is the same as the value that its JSON text reads: its keys are Strings, such as `"theme"` for
344
+ `:theme`, a `Symbol` within it is its text, and a `BigDecimal`, `Date`, `Time` or `DateTime` within it is the text that
345
+ IOStreams writes for it to JSON, such as `"12.5"`, see
346
+ [Values of a field without a type](#values-of-a-field-without-a-type). Any other value is kept as it is, so that a
347
+ value of another class, such as a `Rational`, can differ from the value that its JSON text reads. Hashes and Arrays
348
+ nested more than 100 deep, as JSON reads them, raise `IOStreams::Errors::InvalidValue`, as does a `Float` within them
349
+ that is not finite, such as `Float::NAN`, which JSON cannot hold, when it is written, in every format, naming the field,
350
+ also in an application that uses ActiveSupport, as Rails does, which would write it as `null`. Frictionless keeps a
351
+ dict as it is, and reads a `boolean` only from text, and `true` or `false`, so this conversion, and reading the number
352
+ `1` or `0` as a `boolean`, are what IOStreams adds.
353
+
354
+ A value that is not one of the type of its field, such as `N/A` for an `integer`, raises
355
+ `IOStreams::Errors::InvalidValue`, naming the field and the value, both when reading and when writing. So does a `date`
356
+ or `datetime` that does not exist, such as `2026-02-30`.
357
+
358
+ JSON writes integers, numbers and booleans as JSON numbers and booleans, with all of the digits of a `number`, and the
359
+ digits after the decimal point that its `decimals` give, such as `12.50`, or none for a whole number without
360
+ `decimals`, such as `42`, the same as CSV, an `object` or an `array` as itself, and every other value as text, such as
361
+ a date, including in an application that uses ActiveSupport, as Rails does, which replaces how Ruby writes JSON.
362
+ Reading JSON keeps all of the digits of a `number` too, rather than reading it through a `Float`, rounded to its
363
+ `decimals` when it has them, while the numbers of the other fields are a `Float`, as they are without a schema. The
364
+ `:hash` and `:array` formats keep the values themselves, apart from a `number` with `decimals`, which is rounded to
365
+ them, both when reading and when writing.
366
+
367
+ A `number` field needs the `bigdecimal` gem, which is a bundled gem from Ruby 3.4, so add `gem "bigdecimal"` to the
368
+ Gemfile, or set `floatNumber: true` to read a `Float` instead.
369
+
370
+ ### Matching columns to fields
371
+
372
+ Each column of the header row, or key of a JSON record, is matched to the field:
373
+
374
+ 1. Whose name is the column, such as `email` for the column `email`, whether the name is a String or a Symbol.
375
+ 2. Whose `aliases` include the column, such as `["DOB", "Birth Date"]` for the field `date_of_birth`.
376
+ 3. Whose name or alias is the same as the column once both are compared without their case, spaces and
377
+ punctuation, and with the forms of a character that Unicode treats as the same made the same. So the field
378
+ `first_name` matches the columns `First Name`, `FIRST.NAME` and `firstname`, the field `email` matches `E-mail`,
379
+ and `id` matches the full width `ID` that some spreadsheets have.
380
+ 4. Whose name or alias is the same as the column once both are cleansed as `cleanse_header` cleanses a header row,
381
+ see [Header options](#header-options), and then compared the same way. Unlike `cleanse_header` without a schema,
382
+ which keeps only the ASCII letters and digits, this cleansing keeps the letters and digits of every language, as
383
+ IOStreams 3.0 cleanses a header row, so that a field named as a header row is cleansed matches it in both. It drops
384
+ the characters that Unicode otherwise makes digits, such as `²`, `½` and `①`, so that a field named as the
385
+ cleansed column, such as `area_m` for `Area (m²)`, matches it, as `area_m2` does.
386
+
387
+ A column that is the same as several fields in step 3, or as none, is the field whose name or alias is the column
388
+ once cleansed, if there is one. So a schema whose fields are named as a header row is cleansed finds every column
389
+ that the cleansing named, such as the fields `e_mail` and `email`, which are the same once compared loosely, for the
390
+ columns `E-mail` and `Email`.
391
+
392
+ The columns are matched once, when the header row is read, or once for each set of keys of the JSON records, which
393
+ are usually the same for every record, rather than for each value.
394
+
395
+ The keys of the records read are the names of their fields as they are given, so a field named with a Symbol, such as
396
+ `name: :id`, returns the key `:id`, whatever the column of the file is called, unless `keyType` forms them otherwise,
397
+ see [Keys of the records read](#keys-of-the-records-read). For a format with a header row, such as CSV,
398
+ `each(:array)` returns a header row of the keys of the fields of its columns, and then the values of those fields of
399
+ each row, in the order of the fields. A file without a header row, such as JSON or fixed width, or with a schema whose
400
+ `header` is false, returns the keys of the fields as its header row, and then the values of every field of each row,
401
+ in order, `nil` for a key that a JSON record does not have. The header row is an
402
+ `IOStreams::Row::HeaderRow`, an Array that says that it is the header row, so that code that reads the rows can skip
403
+ it with `next if row.is_a?(IOStreams::Row::HeaderRow)`. A column that the schema does not list is skipped, or raises,
404
+ see [fieldsMatch](#fieldsmatch).
405
+
406
+ A column that could be either of two fields raises `IOStreams::Errors::InvalidHeader`, rather than guess, such as
407
+ `amount` for the fields `Amount ($)` and `Amount (€)`, which match their own columns exactly. So do two columns that
408
+ are the same field, such as `email` and `Email`, rather than lose the value of one of them.
409
+
410
+ When writing, the header row is the names of the fields, in order, and the keys of each record written are matched
411
+ to the fields in the same way. `writer(:array)` writes that header row itself, before the rows, and takes each row
412
+ written as the values of the fields, in order, whatever the format:
413
+
414
+ ~~~ruby
415
+ path = IOStreams.path("people.csv").schema(
416
+ fields: [
417
+ {name: "name"},
418
+ {name: "age", type: "integer"}
419
+ ]
420
+ )
421
+ path.writer(:array) do |io|
422
+ io << ["Jack", 21]
423
+ end
424
+ path.read
425
+ # => "name,age\nJack,21\n"
426
+ ~~~
427
+
428
+ A header row written to it, such as the `IOStreams::Row::HeaderRow` that `each(:array)` yields with a schema, gives
429
+ the columns of the rows after it instead, without a line of its own, and its columns are matched to the fields in the
430
+ same way, so that the values of each row after it can be in any order, and the rows read from another file with a
431
+ schema can be written as they are, by a loop over `each(:array)`. A field that the header row does not have is
432
+ written as empty. `copy_from(source, mode: :array)` hands the target the header row of the source in the same way,
433
+ also when the source has no schema, and skips the blank rows of the source.
434
+
435
+ So a file can have readable column names, while the code uses its own keys:
436
+
437
+ ~~~ruby
438
+ report = IOStreams.path("report.csv").schema(
439
+ fields: [
440
+ {name: "First Name"},
441
+ {name: "Date of Birth", type: "date"}
442
+ ]
443
+ )
444
+ report.writer(:hash) do |io|
445
+ io << {first_name: "Jack", date_of_birth: Date.new(2001, 2, 3)}
446
+ end
447
+ report.read
448
+ # => "First Name,Date of Birth\nJack,2001-02-03\n"
449
+ ~~~
450
+
451
+ ### Keys of the records read
452
+
453
+ `keyType`, which IOStreams adds to the properties of a schema, forms the key of each field in the records read from
454
+ its name:
455
+
456
+ | `keyType` | The key of the field named `First Name` |
457
+ |-----------|-----------------------------------------|
458
+ | None, the default | `"First Name"`, the name as it is given, a String or a Symbol |
459
+ | `string` | `"First Name"`, also for a name that is a Symbol |
460
+ | `cleansed` | `"first_name"`, the name once cleansed as `cleanse_header` cleanses a header row, see [Header options](#header-options), keeping the letters of every language |
461
+ | `symbol` | `:"First Name"`, also for a name that is a String |
462
+ | `cleansedSymbol` | `:first_name` |
463
+
464
+ So a file can have readable column names, which are the names of the fields, while the code reads the keys that it
465
+ needs:
466
+
467
+ ~~~ruby
468
+ path = IOStreams.path("people.csv").schema(
469
+ fieldsMatch: "partial",
470
+ keyType: "cleansedSymbol",
471
+ fields: [
472
+ {name: "First Name"},
473
+ {name: "Date of Birth", type: "date"}
474
+ ]
475
+ )
476
+ path.each(:hash) do |person|
477
+ p person
478
+ end
479
+ # "First Name,Date of Birth\nJack,2001-02-03\n" => {first_name: "Jack", date_of_birth: #<Date: 2001-02-03 ...>}
480
+
481
+ path.writer(:hash) do |io|
482
+ io << {first_name: "Jill", date_of_birth: Date.new(2002, 3, 4)}
483
+ end
484
+ # => "First Name,Date of Birth\nJill,2002-03-04\n"
485
+ ~~~
486
+
487
+ The header row written is still the names of the fields, and a record written can be keyed by the keys of the
488
+ records read, as well as by the names, so that the records read can be written back. A schema held as JSON, or in a
489
+ database such as MongoDB, has String names, so `keyType: "symbol"` keeps the Symbol keys that a schema in Ruby has,
490
+ such as the schema of a layout, see `IOStreams::Schema.from_layout` in
491
+ [Moving a layout to a schema](upgrading#moving-a-layout-to-a-schema). Two fields with the same key, such as
492
+ `First Name` and `first-name` with `cleansed`, raise `ArgumentError`, since a record could not hold both, and so does
493
+ a key that is the name or an alias of another field.
494
+
495
+ ### fieldsMatch
496
+
497
+ `fieldsMatch` says how the columns of the file must match the fields, as Frictionless defines it:
498
+
499
+ | `fieldsMatch` | The columns of the file | Columns that the schema does not list |
500
+ |---------------|-------------------------|---------------------------------------|
501
+ | `exact`, the default | are the fields, in the same order | raise |
502
+ | `equal` | are the fields, in any order | raise |
503
+ | `subset` | include every field | are skipped |
504
+ | `superset` | are fields, but a field can be missing | raise |
505
+ | `partial` | include at least one field | are skipped |
506
+
507
+ A file that does not match raises `IOStreams::Errors::InvalidHeader`, which lists each problem, such as the columns
508
+ that are not fields, and the fields that are missing. The columns without a name at the end of the header row, such
509
+ as the empty column that a spreadsheet exports after the last one, or after a comma at the end of each line, are not
510
+ columns, so a value under one is a value after the last column, see below. A column without a name between other
511
+ columns is a column that the schema does not list.
512
+
513
+ `partial` is the easiest to start with, and suits a schema that only gives the type of some of the columns. A schema
514
+ for files that are received from elsewhere, such as uploads, is usually stricter: `subset` accepts files with more
515
+ columns than the code needs, and `equal` or `exact` accept only the columns that the schema lists.
516
+
517
+ The keys of each JSON record are checked the same way, in any order. The columns of a file without a header row are
518
+ the fields in order, so `header: false` only accepts `exact`, and a fixed width file always has every field.
519
+
520
+ A row read that has a value after the last column of its header row, or after the last field of a file without one,
521
+ such as when a value has a comma that is not quoted, raises `IOStreams::Errors::InvalidHeader` too, unless
522
+ `fieldsMatch` skips it, rather than lose the value. A blank value there, such as after a comma at the end of each
523
+ line, is not data, so it is ignored.
524
+
525
+ A file that is written always has every field, so writing only checks the keys of each record, the columns of the
526
+ header row of the rows written, and the values of each row after the last field or column: `subset` and `partial`
527
+ skip them, and the others raise `IOStreams::Errors::InvalidHeader`. A field that a record does not supply is written
528
+ as empty, or as `null` in JSON.
529
+
530
+ ### Keeping the other columns
531
+
532
+ `extraColumns: "keep"`, which IOStreams adds to the properties of a schema, keeps the columns that `subset` and
533
+ `partial` would skip, after the fields, in the order of the columns, each by the key that
534
+ [keyType](#keys-of-the-records-read) forms from its column, and with its value as it is. So a file can need some
535
+ columns, and the code still read every other column that it has:
536
+
537
+ ~~~ruby
538
+ path = IOStreams.path("upload.csv").schema(
539
+ fieldsMatch: "subset",
540
+ extraColumns: "keep",
541
+ keyType: "cleansed",
542
+ fields: [{name: "zip_code", constraints: {required: true}, allowBlank: true}]
543
+ )
544
+ path.each(:hash) do |record|
545
+ p record
546
+ end
547
+ # "Zip Code,First Name,Notes\n12345,Jack,hi\n" => {"zip_code" => "12345", "first_name" => "Jack", "notes" => "hi"}
548
+ ~~~
549
+
550
+ With `subset`, a schema can then have no fields, to read every column of every file, keyed as `keyType` forms them,
551
+ or as the columns are written without it. The keys of each JSON record are kept the same way, so its rows, which have
552
+ a header row of the keys of the fields, cannot be read with `each(:array)`, which raises `ArgumentError`. A fixed
553
+ width file has only its fields, so it ignores `extraColumns`. Two columns kept with the same key, such as `Notes` and
554
+ `notes` with `keyType: "cleansed"`, raise `IOStreams::Errors::InvalidHeader`, rather than lose one of them, and a
555
+ column without a name is not kept. Writing writes only the fields, whichever it is. The default, `"skip"`, leaves them
556
+ out, and any other `fieldsMatch` raises `ArgumentError` for `"keep"`, since it does not accept them.
557
+
558
+ ### Required fields
559
+
560
+ A field whose `constraints` are `required`, as Frictionless defines it, must have a value: a header row, or JSON
561
+ record, without the field raises `IOStreams::Errors::InvalidHeader`, whatever `fieldsMatch` is, and a value that is
562
+ `nil`, or empty text, raises `IOStreams::Errors::InvalidValue`, both when reading and when writing, including a blank
563
+ record or row written, such as `{}`, which is otherwise written as a blank line. A blank row read, a line that is empty
564
+ or only whitespace, or whose every value is `nil` or blank text, such as a line of CSV of only commas, is `nil`
565
+ instead, as a blank line is. Text that is only spaces is a value for a `string` or `any` field of a row that has other
566
+ values, as in Frictionless, while any other type, such as an `integer`, raises `IOStreams::Errors::InvalidValue` for it,
567
+ see [Types](#types).
568
+
569
+ ~~~ruby
570
+ # An upload, whose columns may be in any order, with names that people typed, and columns that the code ignores.
571
+ schema = {
572
+ fieldsMatch: "subset",
573
+ fields: [
574
+ {name: "first_name", constraints: {required: true}},
575
+ {name: "email", aliases: ["E-mail Address"], constraints: {required: true}},
576
+ {name: "date_of_birth", type: "date", aliases: ["DOB", "Birth Date"]}
577
+ ]
578
+ }
579
+ IOStreams.path("upload.csv").schema(schema).each(:hash) do |contact|
580
+ p contact
581
+ end
582
+ # "E-mail Address,FIRST NAME,DOB,Notes" =>
583
+ # {"first_name" => "Jack", "email" => "j@x.com", "date_of_birth" => #<Date: 2001-02-03 ((2451944j,0s,0n),+0s,2299161j)>}
584
+ ~~~
585
+
586
+ A file can need a column whose values can be blank, such as a middle name that must be in every upload, but that
587
+ many people do not have. Give its field `allowBlank: true` with `required`: a header row, or JSON record, without
588
+ the field still raises, while a value that is `nil`, or empty text, is read and written as `nil`:
589
+
590
+ ~~~ruby
591
+ schema = {
592
+ fieldsMatch: "partial",
593
+ fields: [
594
+ {name: "first_name", constraints: {required: true}},
595
+ {name: "middle_name", constraints: {required: true}, allowBlank: true}
596
+ ]
597
+ }
598
+ IOStreams.path("upload.csv").schema(schema).each(:hash) do |person|
599
+ p person
600
+ end
601
+ # "First Name,Middle Name\nJack,\n" => {"first_name" => "Jack", "middle_name" => nil}
602
+ # "First Name\nJack\n" => IOStreams::Errors::InvalidHeader, missing the required field "middle_name"
603
+ ~~~
604
+
605
+ `allowBlank` is a property of the field that IOStreams adds, rather than a constraint, since Frictionless defines
606
+ `required` as `true` or `false`, and a tool reports an error for a constraint that it does not support, while it
607
+ accepts other properties of a field. A field that is not `required` raises `ArgumentError` for it, since its values
608
+ can be blank anyway.
609
+
610
+ IOStreams does not support the other constraints of Frictionless yet, such as `maxLength`, so they raise
611
+ `ArgumentError`, rather than appear to check values that they do not.
612
+
613
+ ### Schema properties
614
+
615
+ * `fields` The fields of the file, in the order of its columns, which can be empty with `extraColumns: "keep"`.
616
+ Required.
617
+ * `fieldsMatch` How the columns of the file must match the fields, see [fieldsMatch](#fieldsmatch). Default: `exact`
618
+ * `header` `false` when the file has no header row, as in a Frictionless Table Dialect. The columns of the file are
619
+ then the fields, in order: reading takes every line as a row, after the names of the fields as the header row that
620
+ `each(:array)` yields, and writing writes no header row, and the fields in order. A format without a header row, such
621
+ as JSON or fixed width, ignores it. Default: `true`
622
+ * `width` The length of every line of a fixed width file, see
623
+ [Fixed width files with a schema](#fixed-width-files-with-a-schema).
624
+ * `keyType` How the keys of the records read are formed from the names of the fields: `string`, `cleansed`, `symbol`
625
+ or `cleansedSymbol`, see [Keys of the records read](#keys-of-the-records-read). IOStreams adds this property.
626
+ Default: the names as they are given.
627
+ * `extraColumns` `keep` to keep the columns that the schema does not list, which `subset` and `partial` skip, see
628
+ [Keeping the other columns](#keeping-the-other-columns). IOStreams adds this property. Default: `skip`
629
+
630
+ `$schema`, `title` and `description` describe the schema to people and have no effect.
631
+
632
+ ~~~ruby
633
+ # A CSV file without a header row.
634
+ path = IOStreams.path("people.csv").schema(
635
+ header: false,
636
+ fields: [
637
+ {name: "name"},
638
+ {name: "age", type: "integer"}
639
+ ]
640
+ )
641
+ path.each(:hash) do |person|
642
+ p person
643
+ end
644
+ # => {"name" => "Jack", "age" => 21}
645
+ ~~~
646
+
647
+ A column of a file without a header row that the code does not read still needs a field, which holds its place, so
648
+ give it `skip: true`, and the records read leave it out:
649
+
650
+ ~~~ruby
651
+ path = IOStreams.path("people.csv").schema(
652
+ header: false,
653
+ fields: [
654
+ {name: "name"},
655
+ {name: "unused", skip: true},
656
+ {name: "age", type: "integer"}
657
+ ]
658
+ )
659
+ path.each(:hash) do |person|
660
+ p person
661
+ end
662
+ # "Jack,x,21\n" => {"name" => "Jack", "age" => 21}
663
+ ~~~
664
+
665
+ The rows read leave it out too, and writing writes an empty value in its column, from records, and from rows of the
666
+ values of the other fields, in order, as the rows read have them.
667
+
668
+ Since the schema is set on the path, every reader of the path reads the file the same way, including `#copy_from`
669
+ and `#copy_to`, whose options only apply to the target.
670
+
671
+ ### Field properties
672
+
673
+ * `name` The key of the field in each record, and its column in the header row written. Required.
674
+ * `type` See [Types](#types). Default: `any`
675
+ * `constraints` `{required: true}` for a field that must have a value, see [Required fields](#required-fields).
676
+ * `format` For a `date` or a `datetime`, the strptime pattern of its text, such as `"%d/%m/%Y"` or `"%Y%m%d"`, in
677
+ the syntax of Python's strptime, as Frictionless defines it: `%Y` reads exactly 4 digits, so that `31/10/26` raises
678
+ `IOStreams::Errors::InvalidValue` with `%d/%m/%Y`, as do `%G` and the year of a directive that reads several, such
679
+ as `%F` and `%c`, while `%y` reads exactly 2 digits, the time of a `date` or a `datetime` must be on a clock, so
680
+ that the hour `24` of `%H`, such as `24:00`, and a leap second, `60` of `%S`, raise, rather than read the next day
681
+ or the second before it, or be ignored by a `date`, and `%f` reads 1 to 6 digits of microseconds, such as `5` for
682
+ half a second, and writes 6, such as `500000`. Nothing is read on the day that it is read, as Ruby's strptime
683
+ reads the parts of a date that the text leaves out: a pattern that reads a day of the month, `%d` or `%e`, without
684
+ a year, `%Y`, `%y` or `%G`, such as `%m/%d`, raises `ArgumentError` when the schema is created, as Python's strptime
685
+ does from 3.15, since it cannot read February 29: add a year to it, and to the text. So does one that reads an ISO
686
+ 8601 week without its year, `%G`, its week, `%V`, and a weekday, such as `%u`, which give its date together. Text
687
+ without a year, such as that of `%H:%M`, `%m %H:%M` or `%I:%M %p`, is on 1900-01-01, the year, month and day that
688
+ Python gives the parts that it leaves out, such as `1900-07-01 09:30` for `07 09:30` with `%m %H:%M`, the day of
689
+ the year of `%j` counts from 1900-01-01, and a weekday or a week, such as `%a` or `%U`, is ignored. A pattern that
690
+ starts with `fmt:`, as Data Package v0 wrote it, such as `"fmt:%d/%m/%Y"`, has that prefix removed, so that
691
+ `"fmt:default"` is the default form. Ruby's own directives, such as `%N`, `%L` and `%s`, also work, but other
692
+ Frictionless tools do not read them, and `%s` reads whole seconds, ignoring a fraction after it, such as that of
693
+ `%s.%f`. Default: ISO 8601.
694
+ * `trueValues` and `falseValues` For a `boolean`, the text that is true and false.
695
+ Default: `["true", "True", "TRUE", "1"]` and `["false", "False", "FALSE", "0"]`
696
+ * `floatNumber` For a `number`, `true` to read a `Float` rather than a `BigDecimal`, as frictionless-py does.
697
+ * `aliases` The other names of the column of the field, see [Matching columns to fields](#matching-columns-to-fields).
698
+ IOStreams adds this property.
699
+ * `allowBlank` For a field whose `constraints` are `required`, `true` to accept a value that is blank, `nil` or empty
700
+ text, while the file must still have its column, see [Required fields](#required-fields). Text of only whitespace is
701
+ not blank, so it is still read as the type of the field, see [Types](#types). IOStreams adds this property.
702
+ * `decimals` For a `number`, the number of digits after the decimal point, rounding a `BigDecimal` half up, and a
703
+ `Float`, with `floatNumber: true`, as Ruby's `format` writes it, as the layouts of fixed width files always have,
704
+ which rounds a `Float` whose text ends in 5 to the even digit, whatever its binary value, such as `0.12` for `0.125`,
705
+ `0.02` for `0.025` and `2.68` for `2.675` with `decimals: 2`, and `2.0` for `2.5` with `decimals: 0`. It rounds the
706
+ number read too, such as the
707
+ `BigDecimal` `12.35` for `12.345` with `decimals: 2`, also a number that is not text, such as one of JSON, and the
708
+ number written to the `:hash` and `:array` formats, so that a record read holds the number that is written. A
709
+ number that rounds to zero is zero, rather than `-0.0`. IOStreams adds this property.
710
+ * `skip` `true` for a field that only holds the place of its column, such as a column of a file without a header row
711
+ that the code does not read: reading leaves it out of the rows and records, and writing writes an empty value in its
712
+ column, or leaves it out of a JSON record. It takes only the properties that place its column, `name`, `aliases`,
713
+ `width` and `start`. IOStreams adds this property.
714
+ * `width`, `start` and `truncate` The position of the field in a fixed width file, see
715
+ [Fixed width files with a schema](#fixed-width-files-with-a-schema). IOStreams adds these properties. The other
716
+ formats ignore them, so that one schema can describe both a fixed width file and the CSV file that it is converted
717
+ to.
718
+
719
+ `title`, `description`, `example` and `rdfType` describe a field to people and have no effect, as do the properties
720
+ of other tools, whose names include a `:`, such as `"mytool:owner"`. Any other property raises `ArgumentError`, as do
721
+ the parts of Frictionless that IOStreams does not support yet, such as `missingValues`, `primaryKey`, the constraints
722
+ other than `required`, and the `time` type, so that a schema never appears to check a value that it does not.
723
+
724
+ ### Values of a field without a type
725
+
726
+ A value of a field without a type, `any`, is written as follows:
727
+
728
+ | Value | Written as |
729
+ |-------|------------|
730
+ | `BigDecimal` | a plain decimal, such as `12.5`, rather than `0.125e2` |
731
+ | `Float` or `BigDecimal` that is not finite | `NaN`, `INF` or `-INF`, as the Frictionless spec spells it, rather than `Infinity`, while JSON, which cannot hold it, raises `IOStreams::Errors::InvalidValue` for it, unless it is inside a Hash or an Array, which writes a `BigDecimal` as that text, and raises `IOStreams::Errors::InvalidValue` for a `Float` in every format, since its JSON text cannot hold it |
732
+ | `Time` or `DateTime` | ISO 8601, such as `2026-10-13T09:30:00Z`, with as many digits of its fraction of a second as it needs, and `Z` for a `DateTime` at an offset of zero, as for a `Time` in UTC |
733
+ | `Date` | ISO 8601, such as `2026-10-13` |
734
+ | `Hash` or `Array` | JSON, such as `{"a":1}` or `[1,2]`, as the JSON format writes it |
735
+ | any other value | its `to_s`, such as `42` or `true` |
736
+
737
+ JSON writes a `BigDecimal`, `Date` or `Time` as that text, within an object or an array too, so that a `BigDecimal`
738
+ keeps all of its digits, and writes every other value as itself, such as a number or `true`. The `:hash` and `:array`
739
+ formats keep the values themselves. To write a value in another form, such as a date as `13/10/2026`, give its field
740
+ a type and a `format` in the schema.
741
+
742
+ A file without a schema writes each value as before, such as a `BigDecimal` as its `to_s`, `0.125e2`, including the
743
+ values that a copy reads from a file with a schema.
744
+
745
+ ### Fixed width files with a schema
746
+
747
+ The schema gives the `width` of each field of a fixed width file, in the order of the line, in place of the layout of
748
+ `format_options`:
749
+
750
+ ~~~ruby
751
+ path = IOStreams.path("sample_data").format(:fixed).schema(
752
+ fields: [
753
+ {name: "name", type: "string", width: 23},
754
+ {name: "address", type: "string", width: 40},
755
+ {name: "zip", type: "integer", width: 5}
756
+ ]
757
+ )
758
+ path.each(:hash) do |record|
759
+ p record
760
+ end
761
+
762
+ path.writer(:hash) do |io|
763
+ io << {"name" => "Jack Jones", "address" => "Somewhere", "zip" => 12345}
764
+ end
765
+ ~~~
766
+
767
+ A fixed width file has no header row, so `each(:array)` yields the names of the fields as its header row, and then
768
+ the values of the fields of each line, in order. `writer(:array)` takes rows of the values of the fields, in order, and
769
+ takes the header row that `each(:array)` yields as the header row of the rows after it.
770
+
771
+ The properties that place each field:
772
+
773
+ * `width` The number of characters of the field. The last field may have a width of `"remainder"` to take the rest
774
+ of the line.
775
+ * `start` The position of the first character of the field, counting from 1, as the specifications of fixed width
776
+ files do. It skips the characters after the field before it, such as a filler.
777
+ Default: right after the field before it.
778
+ * `truncate` For a `string`, `true` to write only the first `width` characters of a longer value. Without it, a value
779
+ that is too long raises `IOStreams::Errors::ValueTooLong`. A number is never truncated, and raises instead.
780
+
781
+ The `width` at the top level of the schema is the length of every line, when the line ends after the last field, such
782
+ as after a filler, for example `{"fields": [...], "width": 94}`. A `width` of `"remainder"` accepts lines of any
783
+ length, ignoring what follows the last field. Otherwise every line ends where the last field ends. A line of another
784
+ length raises `IOStreams::Errors::InvalidLineLength`.
785
+
786
+ When reading, each value is the text of its field without its leading and trailing spaces, converted to the type of
787
+ its field, so that a field of only spaces is empty text, a missing value, `nil`, see [Types](#types). When writing, a
788
+ number is right justified and zero filled, such as `000042` for `42` without `decimals`, or `042.00` with
789
+ `decimals: 2`, and a missing number is written as zero, as the specifications of most fixed width files require. A
790
+ number that is not finite, such as `NaN` or `-INF`, is space filled instead, so that it can be read back. Any other
791
+ value is left justified and space filled, with each line break replaced with a space, so that a value cannot add
792
+ records. The characters that no field covers, such as a filler, are spaces.
793
+
794
+ ### Schemas in IOStreams 2.2 and 3.0
795
+
796
+ A schema reads and writes the rows and records of a file in IOStreams 2.2 as IOStreams 3.0 does, so that code that
797
+ uses one works the same way after an upgrade, apart from these differences, which IOStreams 3.0 makes to every file:
798
+
799
+ * The text of a file read with a schema is UTF-8, see [Schema](#schema), but IOStreams 2.2 does not remove the byte
800
+ order mark at the start of a UTF-8 file, which is still part of the first column of the header row, which is still
801
+ matched to its field, see [Matching columns to fields](#matching-columns-to-fields), and leaves the line endings as
802
+ they are. An encoding set on the encode stream decodes the text as it does without a schema, and its text is not
803
+ checked.
804
+ * The positions of the fields of a fixed width file count the characters of its UTF-8 text, while IOStreams 3.0 reads
805
+ a fixed width file as ASCII, so that a character that is not ASCII raises. Set the encoding of a file that is not
806
+ ASCII with `#encoding`, so that both read it the same way: `encoding("UTF-8")` for UTF-8 text whose sizes count
807
+ characters, `encoding("BINARY")` to read binary strings, whose positions count bytes, or the name of a single-byte
808
+ code page, such as `encoding("ISO-8859-1")` for Latin-1, whose values are then strings in that encoding.
809
+ IOStreams 3.0 also takes `encoding("ISO-8859-1:UTF-8")`, to read UTF-8 strings.
810
+ * A file without a schema is read and written as before: its `each(:array)` yields its header row as an Array,
811
+ rather than an `IOStreams::Row::HeaderRow`, so a loop that writes its rows to a writer with a schema writes the
812
+ header row as a row of values, while `copy_from(source, mode: :array)` hands it over as the header row. A copy
813
+ between two files without a schema copies each row as before.
814
+ * Parquet files are only in IOStreams 3.0, which removes the layout of `format_options`, while IOStreams 2.2 reads and
815
+ writes it with the layout, as before, and `IOStreams::Schema.from_layout` returns its schema, see
816
+ [Moving a layout to a schema](upgrading#moving-a-layout-to-a-schema).