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.
- checksums.yaml +4 -4
- data/docs/config.md +13 -9
- data/docs/copy_files.md +20 -2
- data/docs/extensions.md +18 -4
- data/docs/formats.md +644 -16
- data/docs/path.md +18 -2
- data/docs/streams.md +54 -9
- data/docs/tutorial.md +14 -2
- data/docs/upgrading.md +470 -23
- data/lib/io_streams/builder/deprecations.rb +250 -0
- data/lib/io_streams/builder.rb +256 -39
- data/lib/io_streams/bzip2/reader.rb +18 -2
- data/lib/io_streams/bzip2/writer.rb +11 -2
- data/lib/io_streams/deprecation/methods.rb +51 -0
- data/lib/io_streams/deprecation/once.rb +27 -0
- data/lib/io_streams/deprecation/option_names.rb +26 -0
- data/lib/io_streams/deprecation/words.rb +42 -0
- data/lib/io_streams/deprecation.rb +259 -0
- data/lib/io_streams/encode/reader.rb +54 -3
- data/lib/io_streams/encode/writer.rb +19 -2
- data/lib/io_streams/encode.rb +99 -0
- data/lib/io_streams/errors.rb +51 -0
- data/lib/io_streams/gzip/reader.rb +11 -0
- data/lib/io_streams/io_streams.rb +365 -35
- data/lib/io_streams/line/reader.rb +2 -1
- data/lib/io_streams/line/utf8_reader.rb +23 -0
- data/lib/io_streams/path.rb +125 -35
- data/lib/io_streams/paths/file.rb +72 -9
- data/lib/io_streams/paths/http.rb +48 -2
- data/lib/io_streams/paths/matcher.rb +6 -3
- data/lib/io_streams/paths/s3.rb +180 -9
- data/lib/io_streams/paths/sftp.rb +47 -3
- data/lib/io_streams/paths.rb +23 -0
- data/lib/io_streams/pgp/reader.rb +18 -0
- data/lib/io_streams/pgp/writer.rb +16 -0
- data/lib/io_streams/pgp.rb +16 -2
- data/lib/io_streams/reader.rb +44 -0
- data/lib/io_streams/record/reader.rb +59 -68
- data/lib/io_streams/record/writer.rb +17 -10
- data/lib/io_streams/row/header_row.rb +21 -0
- data/lib/io_streams/row/reader.rb +52 -15
- data/lib/io_streams/row/writer.rb +64 -13
- data/lib/io_streams/schema/columns.rb +456 -0
- data/lib/io_streams/schema/constraints.rb +99 -0
- data/lib/io_streams/schema/field.rb +967 -0
- data/lib/io_streams/schema/json_number.rb +56 -0
- data/lib/io_streams/schema/keys.rb +78 -0
- data/lib/io_streams/schema/pattern.rb +257 -0
- data/lib/io_streams/schema/source.rb +31 -0
- data/lib/io_streams/schema.rb +616 -0
- data/lib/io_streams/stream.rb +836 -67
- data/lib/io_streams/symmetric_encryption/reader.rb +8 -0
- data/lib/io_streams/tabular/header.rb +29 -0
- data/lib/io_streams/tabular/header_mapping.rb +332 -0
- data/lib/io_streams/tabular/mapping.rb +165 -0
- data/lib/io_streams/tabular/parser/array.rb +5 -0
- data/lib/io_streams/tabular/parser/base.rb +24 -0
- data/lib/io_streams/tabular/parser/fixed.rb +197 -0
- data/lib/io_streams/tabular/parser/fixed_fields.rb +160 -0
- data/lib/io_streams/tabular/parser/hash.rb +10 -0
- data/lib/io_streams/tabular/parser/json.rb +17 -0
- data/lib/io_streams/tabular/parser/json_fields.rb +132 -0
- data/lib/io_streams/tabular/schema_mapping.rb +361 -0
- data/lib/io_streams/tabular.rb +344 -30
- data/lib/io_streams/utils.rb +441 -6
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/writer.rb +27 -0
- data/lib/io_streams/xlsx/cells.rb +127 -0
- data/lib/io_streams/xlsx/reader.rb +56 -0
- data/lib/io_streams/xlsx/schema_reader.rb +90 -0
- data/lib/io_streams/xlsx/v3_writer.rb +16 -0
- data/lib/io_streams/zip/reader.rb +7 -0
- data/lib/io_streams/zip/writer.rb +24 -2
- data/lib/iostreams.rb +21 -17
- 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
|
|
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.
|
|
57
|
-
|
|
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,
|
|
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.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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).
|