lesli_date 1.0.0 → 1.1.1
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/CHANGELOG.md +31 -0
- data/Rakefile +3 -3
- data/docs/about/configuration.md +100 -0
- data/docs/about/development.md +131 -0
- data/docs/about/installation.md +48 -0
- data/docs/api/database.md +89 -0
- data/docs/api/examples.md +113 -0
- data/docs/api/formatter.md +104 -0
- data/docs/api/index +6 -0
- data/docs/api/overview.md +26 -0
- data/docs/api/parsing.md +101 -0
- data/docs/images/date-logo.svg +1 -0
- data/docs/navigation +2 -0
- data/lib/lesli_date/errors.rb +8 -0
- data/lib/lesli_date/formatter.rb +99 -75
- data/lib/lesli_date/parser.rb +61 -0
- data/lib/lesli_date/version.rb +2 -2
- data/readme.md +196 -39
- metadata +87 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d64dec0455da16b095e0cb4b67fccb28fb2a3010f97dedcd97e0ac8178e272ca
|
|
4
|
+
data.tar.gz: 1452d2bf0482a4604b26b03341312e3ebab51fdedba716807fd38457cb11162d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bfc90a78e1b1cb23175cf9d3283720048ca882d7268528422ff20b169f67ba78ffe29a26599028cd6c5d0f1d5b3e60ccd84673425c7338a7f29f89feefeb568f
|
|
7
|
+
data.tar.gz: 03e1aa63070d95b58b153413ce7c17d3d0052829f3be572d6d141863ca53d0533f370b441e6c8f6ec6bf2a028d6d46660446ac5c4809fcbed5a767a9838a3a33
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
### Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby 3.3 or later; ActiveSupport and ActiveRecord 8.0.x or 8.1.x.
|
|
8
|
+
- Explicit runtime dependencies on ActiveSupport, ActiveRecord and I18n.
|
|
9
|
+
- Database drivers remain dependencies of the host application.
|
|
10
|
+
|
|
11
|
+
### Parsing and presentation
|
|
12
|
+
|
|
13
|
+
- Preserve temporal instants and native precision; retain calendar dates as `Date`.
|
|
14
|
+
- Use native Ruby parsing and Rails time-zone conversion. UTC is the default
|
|
15
|
+
input zone; external local times can specify `input_zone:`.
|
|
16
|
+
- Apply all configured output formats, with defaults for omitted values.
|
|
17
|
+
- Use existing Rails translations with optional per-formatter `locale:`.
|
|
18
|
+
- Expose parsing and configuration errors through `ArgumentError` subclasses.
|
|
19
|
+
- Keep the fluent selectors, positional signatures, `.get` and Date2 inheritance.
|
|
20
|
+
|
|
21
|
+
### SQL and verification
|
|
22
|
+
|
|
23
|
+
- Quote SQL identifiers and literals with ActiveRecord, without modifying inputs.
|
|
24
|
+
- Return both timestamp columns with shared `*_string` aliases on SQLite and PostgreSQL.
|
|
25
|
+
- Generate expressions without executing validation queries; `as: false` omits an alias.
|
|
26
|
+
- Preserve fractional fixed offsets in SQLite; use per-row IANA conversion in PostgreSQL.
|
|
27
|
+
- Keep native SQLite `NULL` results for unsupported formatting directives.
|
|
28
|
+
- Run standalone LesliTesting suites with real SQLite and PostgreSQL queries.
|
|
29
|
+
- Use the LesliActions gem workflow and test the supported Rails lines in CI.
|
|
30
|
+
|
|
31
|
+
See [application examples](docs/api/examples.md) for forms, APIs, imports and SQL.
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Configuration and presentation
|
|
2
|
+
|
|
3
|
+
LesliDate reads `Lesli.config.datetime` from the Lesli initializer. Database
|
|
4
|
+
timestamps are interpreted as UTC; `time_zone` controls presentation. The fluent
|
|
5
|
+
API stays the same:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
LesliDate::Formatter.new(value).date.to_s
|
|
9
|
+
LesliDate::Formatter.new(value).time.to_s
|
|
10
|
+
LesliDate::Formatter.new(value).date_time.to_s
|
|
11
|
+
LesliDate::Formatter.new(value).date_words.to_s
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Initializer formats
|
|
15
|
+
|
|
16
|
+
Each selector reads the same-named symbol key from `datetime[:formats]`. These
|
|
17
|
+
are the patterns supplied by the Lesli initializer:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
config.datetime = {
|
|
21
|
+
time_zone: "America/Guatemala",
|
|
22
|
+
start_week_on: "monday",
|
|
23
|
+
formats: {
|
|
24
|
+
date: "%d/%m/%Y",
|
|
25
|
+
time: "%H:%M",
|
|
26
|
+
date_time: "%d/%m/%Y %H:%M",
|
|
27
|
+
date_words: "%A, %B %d, %Y",
|
|
28
|
+
date_time_words: "%A, %B %d, %Y, %H:%M",
|
|
29
|
+
date_time_words_pm: "%A, %B %d, %Y, %I:%M %p",
|
|
30
|
+
date_time_words_day: "%A, %B %d, %Y, %H:%M"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`date_words_day` is also configurable. When omitted, it uses its fallback.
|
|
36
|
+
`start_week_on` remains part of Lesli's configuration and does not change how
|
|
37
|
+
this formatter parses or presents a value.
|
|
38
|
+
|
|
39
|
+
## Missing settings and validation
|
|
40
|
+
|
|
41
|
+
The entire `formats` hash is optional: absent, `nil`, or empty hashes use the
|
|
42
|
+
fallback patterns listed in the [Formatter API](/gems/date/api/formatter).
|
|
43
|
+
Partial hashes override only the supplied keys, and individual `nil` values
|
|
44
|
+
also use the fallback. Defaults are merged without modifying the initializer.
|
|
45
|
+
Existing formatters retain their patterns if configuration changes later.
|
|
46
|
+
|
|
47
|
+
Output patterns must be nonempty strings. Empty or whitespace-only strings,
|
|
48
|
+
invalid text encoding, and other value types raise `LesliDate::InvalidFormat`
|
|
49
|
+
when constructing the formatter. A non-hash `formats` value also raises this
|
|
50
|
+
error. Every configured pattern is checked, even if its selector is not used.
|
|
51
|
+
|
|
52
|
+
Syntax checks delegate to Ruby's `strftime`; there is no directive allowlist or
|
|
53
|
+
custom pattern compiler. Directives that Ruby preserves as literal text retain
|
|
54
|
+
that behavior. Native formatting exceptions are exposed as `InvalidFormat`.
|
|
55
|
+
|
|
56
|
+
A valid output zone is required in the initializer unless supplied through
|
|
57
|
+
`zone:`. `input_zone:` defaults to UTC and can be used for external local-time
|
|
58
|
+
sources. Both zone names are validated during construction. Missing or unknown
|
|
59
|
+
zones raise `LesliDate::InvalidTimeZone` rather than using the process timezone.
|
|
60
|
+
|
|
61
|
+
## Native Rails translations
|
|
62
|
+
|
|
63
|
+
`to_s` calls `I18n.localize` with the configured pattern. It uses the backend,
|
|
64
|
+
locale data, fallbacks and customizations already configured by the Rails
|
|
65
|
+
application. LesliDate does not register locales, ship translations, change the
|
|
66
|
+
backend or alter `I18n.locale`.
|
|
67
|
+
|
|
68
|
+
By default, each call to `to_s` uses the current `I18n.locale`. Set `locale:` to
|
|
69
|
+
choose a language for one formatter:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
I18n.with_locale(:es) do
|
|
73
|
+
LesliDate::Formatter.new("2026-10-22").date_words.to_s
|
|
74
|
+
# => "jueves, octubre 22, 2026", with the initializer above
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
LesliDate::Formatter.new("2026-10-22", locale: :en).date_words.to_s
|
|
78
|
+
# => "Thursday, October 22, 2026"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The pattern controls order and punctuation; I18n supplies words for directives
|
|
82
|
+
such as `%A`, `%a`, `%B`, `%b`, `%p` and `%P`. Ruby/Rails also determine casing,
|
|
83
|
+
so `%p` uses uppercase AM/PM text. The input parser is unchanged and does not
|
|
84
|
+
use this output locale to interpret strings.
|
|
85
|
+
|
|
86
|
+
Use the translations Rails already loads, including the application's existing
|
|
87
|
+
Rails locale-data dependencies. Only add keys in Lesli or the host application's
|
|
88
|
+
locale files when customization is desired. For example, an optional override
|
|
89
|
+
file can contain just:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
es:
|
|
93
|
+
time:
|
|
94
|
+
am: "a. m."
|
|
95
|
+
pm: "p. m."
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
All other translations remain with the existing I18n backend. Missing locales
|
|
99
|
+
or translations follow Rails/I18n's configured behavior; LesliDate does not add
|
|
100
|
+
a separate fallback language or translation-loading mechanism.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Development and tests
|
|
2
|
+
|
|
3
|
+
Run commands from the LesliDate gem directory. The suite uses LesliTesting 1.4.2,
|
|
4
|
+
Minitest 6, ActiveRecord/ActiveSupport 8.0.x or 8.1.x, SQLite and PostgreSQL.
|
|
5
|
+
It runs without a Rails or Lesli application. Ruby 3.3 or later is required;
|
|
6
|
+
CI uses 3.3.5. The lockfile includes macOS Intel and Linux x86_64.
|
|
7
|
+
|
|
8
|
+
```shell
|
|
9
|
+
bundle install
|
|
10
|
+
bundle exec rake
|
|
11
|
+
COVERAGE=true bundle exec rake test
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## LesliTesting integration
|
|
15
|
+
|
|
16
|
+
`test/test_helper.rb` requires LesliTesting and configures
|
|
17
|
+
`LesliTesting.gem("LesliDate", coverage_min_coverage: 90)` before loading the gem.
|
|
18
|
+
This activates the shared Minitest reporter and, when `COVERAGE=true`, SimpleCov
|
|
19
|
+
with console and Cobertura output at `coverage/coverage.xml`.
|
|
20
|
+
|
|
21
|
+
`Rakefile` exposes the standard Minitest `test` task as its default. The GitHub
|
|
22
|
+
Actions workflow delegates to
|
|
23
|
+
`LesliTech/LesliActions/.github/workflows/lesli-ci-gem.yaml@master`, which runs
|
|
24
|
+
coverage tests, verifies the XML report, builds the gem, and uploads artifacts.
|
|
25
|
+
Providing `CODECOV_TOKEN` also enables its optional Codecov upload. The reusable
|
|
26
|
+
LesliActions gem job remains the SQLite coverage and packaging job. An adapter
|
|
27
|
+
matrix starts PostgreSQL 17 and runs the same LesliTesting suite in SQLite and
|
|
28
|
+
PostgreSQL for each supported Rails line:
|
|
29
|
+
|
|
30
|
+
| CI lane | Ruby | Rails components | Database |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| LesliActions gem workflow | 3.3.5 | Locked 8.0.x baseline | SQLite |
|
|
33
|
+
| Adapter matrix | 3.3.5 | 8.0.x / 8.1.x | SQLite and PostgreSQL 17 |
|
|
34
|
+
|
|
35
|
+
`RAILS_VERSION` selects matching ActiveRecord and ActiveSupport requirements in
|
|
36
|
+
the development Gemfile. The checked-in lockfile keeps the baseline; the matrix
|
|
37
|
+
allows Bundler to resolve the selected line and uses separate caches. To test a
|
|
38
|
+
different line locally, update the development bundle explicitly:
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
RAILS_VERSION='~> 8.1.0' bundle install
|
|
42
|
+
RAILS_VERSION='~> 8.1.0' bundle exec rake test
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This changes the local lockfile. Run `bundle install` without `RAILS_VERSION`
|
|
46
|
+
to return to the default 8.0.x development requirement before committing.
|
|
47
|
+
|
|
48
|
+
## Controlled test environment
|
|
49
|
+
|
|
50
|
+
Each test receives a fresh `Lesli.config.datetime` with the
|
|
51
|
+
`America/Guatemala` display zone, a Monday week start, `%d.%m.%Y` date format,
|
|
52
|
+
and `%d.%m.%Y %H:%M` date-time format. The helper supplies only the minimal
|
|
53
|
+
Lesli configuration interface; it does not load the framework.
|
|
54
|
+
|
|
55
|
+
The Ruby clock is fixed at `2026-10-22 14:30:45 UTC`, `Time.zone` is UTC, and
|
|
56
|
+
the locale is English. Configuration, clock, zone, and locale are restored after
|
|
57
|
+
each test. Spanish translation fixtures support localization tests without
|
|
58
|
+
booting a Rails application. These fixtures are test data only; LesliDate
|
|
59
|
+
registers no translations at runtime.
|
|
60
|
+
|
|
61
|
+
SQLite uses a dedicated `:memory:` database. The `events` table is reset before
|
|
62
|
+
every test with these UTC timestamps:
|
|
63
|
+
|
|
64
|
+
| Column | Value |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `occurred_at` | `2026-10-22 14:30:45` |
|
|
67
|
+
| `created_at` | `2026-10-22 14:30:45` |
|
|
68
|
+
| `updated_at` | `2026-10-23 15:40:50` |
|
|
69
|
+
|
|
70
|
+
The database's own clock is independent of Ruby's frozen clock. The `db_now` test
|
|
71
|
+
checks the expression and result shape without asserting a wall-clock value.
|
|
72
|
+
|
|
73
|
+
## Test groups
|
|
74
|
+
|
|
75
|
+
| File | Coverage |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `test/lesli_date_test.rb` | Version and build metadata |
|
|
78
|
+
| `test/formatter_test.rb` | Constructor calls, supported inputs, explicit output, chaining, `.get`, configuration, and the Date2 subclass pattern |
|
|
79
|
+
| `test/parser_test.rb` | Calendar dates, native Ruby formats and exceptions, fractions, offsets, zone conversion, and standalone loading |
|
|
80
|
+
| `test/presentation_test.rb` | All initializer formats, defaults, construction-time validation, Rails locales and host translation overrides |
|
|
81
|
+
| `test/database_test.rb` | Real SQL execution, aliases, NULL values, ActiveRecord selects and grouped counts on either adapter |
|
|
82
|
+
| `test/database_regression_test.rb` | Quoting, immutable arguments, fractional offsets, native format limitations and PostgreSQL daylight saving/session independence |
|
|
83
|
+
|
|
84
|
+
## Running PostgreSQL tests
|
|
85
|
+
|
|
86
|
+
SQLite is the default and requires no server. To run the same suite against
|
|
87
|
+
PostgreSQL, supply a test database URL:
|
|
88
|
+
|
|
89
|
+
```shell
|
|
90
|
+
LESLI_DATE_DATABASE_URL=postgresql://localhost/lesli_date_test bundle exec rake test
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The helper creates an isolated `lesli_date_test_<pid>` schema, sets the
|
|
94
|
+
connection's search path to it, and drops that schema after the run. The test
|
|
95
|
+
user needs permission to create schemas. Each test resets the same controlled
|
|
96
|
+
fixture data as SQLite. A failed connection fails the suite; PostgreSQL tests
|
|
97
|
+
are never replaced with SQL-generation stubs or silently skipped.
|
|
98
|
+
|
|
99
|
+
Date2 remains a minimal subclass defined in the test file, independent of the
|
|
100
|
+
application's source tree. The SQL regressions are ordinary passing tests;
|
|
101
|
+
there is no pending-defect wrapper. Tests cover each adapter's formatting
|
|
102
|
+
behavior, including SQLite's native `NULL` for unsupported directives.
|
|
103
|
+
See [Database expressions](../api/database.md) for each
|
|
104
|
+
adapter's formats, zone limitations, and the shared `*_string` aliases.
|
|
105
|
+
|
|
106
|
+
## Reproduce ordering
|
|
107
|
+
|
|
108
|
+
Minitest randomizes test order and prints the seed. Reuse it with the standard
|
|
109
|
+
Minitest task's `A` argument:
|
|
110
|
+
|
|
111
|
+
```shell
|
|
112
|
+
bundle exec rake test A="--seed 1729"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Preparing a release
|
|
116
|
+
|
|
117
|
+
1. Choose the release version in `lib/lesli_date/version.rb`, move the
|
|
118
|
+
`Unreleased` changelog entries to that version, and refresh `Gemfile.lock`.
|
|
119
|
+
2. Run the CI matrix and review the changelog.
|
|
120
|
+
3. Build and inspect the package, which includes the public documentation,
|
|
121
|
+
changelog, runtime source and license:
|
|
122
|
+
|
|
123
|
+
```shell
|
|
124
|
+
COVERAGE=true bundle exec rake test
|
|
125
|
+
bundle exec rake build
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The artifact is `pkg/lesli_date-<version>.gem`. Verify its specification with
|
|
129
|
+
`gem specification pkg/lesli_date-<version>.gem` and publish the selected package
|
|
130
|
+
with `gem push pkg/lesli_date-<version>.gem` after the release version and notes
|
|
131
|
+
are finalized. Building the package does not publish it or create a Git tag.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
LesliDate provides time-zone-aware display formatting and database date expressions for Lesli Rails applications.
|
|
4
|
+
|
|
5
|
+
Add the gem to the application:
|
|
6
|
+
|
|
7
|
+
```shell
|
|
8
|
+
bundle add lesli_date
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
| Requirement | Supported versions / setup |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Ruby | 3.3 or later |
|
|
16
|
+
| ActiveSupport and ActiveRecord | 8.0.x or 8.1.x; Rails keeps their versions aligned |
|
|
17
|
+
| I18n | `>= 1.6`, `< 2`; use the locale data already loaded by Rails |
|
|
18
|
+
| Host configuration | `Lesli.config.datetime`, including a valid output `time_zone` |
|
|
19
|
+
| SQL helpers | An established ActiveRecord connection; SQLite or PostgreSQL |
|
|
20
|
+
|
|
21
|
+
The gem declares ActiveSupport, ActiveRecord and I18n as runtime dependencies.
|
|
22
|
+
The application supplies its database driver (`sqlite3` or `pg`). LesliTesting,
|
|
23
|
+
Rake and test database drivers are development dependencies, not requirements
|
|
24
|
+
installed by this gem. CI tests Ruby 3.3.5, both supported Rails lines,
|
|
25
|
+
SQLite, and PostgreSQL 17. See [Development](/gems/date/about/development).
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
LesliDate reads `Lesli.config.datetime`, so load and configure Lesli before constructing a formatter. A standard Lesli application already provides this configuration.
|
|
30
|
+
|
|
31
|
+
All display formats come from `datetime[:formats]`, with defaults for missing
|
|
32
|
+
values. Presentation uses the translations already loaded by Rails/I18n; no
|
|
33
|
+
LesliDate locale files are required. See [Configuration](/gems/date/about/configuration)
|
|
34
|
+
for output patterns and optional customization in Lesli.
|
|
35
|
+
|
|
36
|
+
Verify the installation in a Rails console:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
LesliDate::Formatter.new(Time.current).date_time.to_s
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The formatter accepts `Date`, `DateTime`, `Time`, `TimeWithZone`, supported
|
|
43
|
+
ISO-style strings, and strings accompanied by an explicit input format. It
|
|
44
|
+
returns itself from its format selectors. `get` returns a `Date` for calendar
|
|
45
|
+
inputs and a `TimeWithZone` for instants. See [Parsing rules](/gems/date/api/parsing)
|
|
46
|
+
for input and presentation zones, supported syntax, and validation errors.
|
|
47
|
+
|
|
48
|
+
LesliDate currently generates adapter-specific database expressions for SQLite and PostgreSQL. See [Database expressions](/gems/date/api/database) before using those helpers with another adapter.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Database expressions
|
|
2
|
+
|
|
3
|
+
LesliDate generates SQL for UTC timestamps stored by Rails: ISO date-time text
|
|
4
|
+
in SQLite and `timestamp without time zone` columns in PostgreSQL. These helpers
|
|
5
|
+
format instants, not calendar-only `date` columns or PostgreSQL `timestamptz`.
|
|
6
|
+
Use `.to_s` for calendar dates and localized presentation.
|
|
7
|
+
|
|
8
|
+
## One column
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
formatter = LesliDate::Formatter.new.date_time
|
|
12
|
+
sql = formatter.db_column("published_at", "articles", as: "published_at_display")
|
|
13
|
+
Article.select("articles.*", sql)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`db_column(column, table = "", as: nil)` keeps the positional column and table
|
|
17
|
+
arguments. Strings and symbols are accepted. The default alias is
|
|
18
|
+
`<column>_string`; `as:` customizes it. Arguments are never modified, including
|
|
19
|
+
frozen strings. ActiveRecord quotes identifiers and SQL literals. Column and
|
|
20
|
+
alias arguments are identifiers, not expressions; table names follow the
|
|
21
|
+
adapter's native naming conventions, including PostgreSQL `schema.table`.
|
|
22
|
+
Generating these SQL fragments does not execute database queries.
|
|
23
|
+
|
|
24
|
+
For grouping, omit the alias explicitly instead of editing the SQL string:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
group = LesliDate::Formatter.new.date.db_column(:created_at, as: false)
|
|
28
|
+
Article.group(Arel.sql(group)).order(Arel.sql("MIN(created_at) ASC")).count
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`as: false` is the only additional option. Existing calls keep their aliases.
|
|
32
|
+
|
|
33
|
+
## Timestamp columns and aliases
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
sql = LesliDate::Formatter.new.date_time.db_timestamps("articles")
|
|
37
|
+
Article.select("articles.*", sql)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Both adapters return `created_at_string` and `updated_at_string`, matching the
|
|
41
|
+
aliases used by Lesli consumers. SQLite now includes `updated_at` as well.
|
|
42
|
+
The previous PostgreSQL aliases `created_at_date` and `updated_at_date` are
|
|
43
|
+
replaced directly; no legacy mode is retained. Consumers needing another name
|
|
44
|
+
can use `db_column(:created_at, as: "created_at_date")` explicitly. SQL `NULL`
|
|
45
|
+
values remain `nil`.
|
|
46
|
+
|
|
47
|
+
## Supported formats
|
|
48
|
+
|
|
49
|
+
The selected pattern comes from `Lesli.config.datetime[:formats]`, just as it
|
|
50
|
+
does for `.to_s`, but SQL uses each database's formatting capabilities:
|
|
51
|
+
|
|
52
|
+
| Adapter | Supported patterns |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| SQLite | Native `strftime` patterns supported by the connected SQLite version. Unsupported directives return SQL `NULL` (`nil`) when the query executes, following SQLite's native behavior. `%Y`, `%m`, `%d`, `%H`, `%M`, `%S`, and `%%` cover the common numeric formats. Month/day names such as `%B` and `%A` are unavailable. |
|
|
55
|
+
| PostgreSQL | `%Y`, `%y`, `%m`, `%d`, `%B`, `%b`, `%A`, `%a`, `%j`, `%F`, `%H`, `%I`, `%p`, `%P`, `%M`, `%S`, `%R`, `%T`, and `%%`. Other directives, flags, and widths raise `InvalidFormat`. Literal text is preserved. |
|
|
56
|
+
|
|
57
|
+
PostgreSQL names and AM/PM indicators use native English formatting, without
|
|
58
|
+
padding month/day names. SQL does not use Rails translations or `locale:`.
|
|
59
|
+
For translated words, format the value in Ruby with `.to_s`, which continues to
|
|
60
|
+
use the host's existing I18n translations. SQLite-specific directives can differ
|
|
61
|
+
from Ruby; use the common numeric directives when matching Ruby output matters.
|
|
62
|
+
|
|
63
|
+
See the native [SQLite strftime documentation](https://www.sqlite.org/lang_datefunc.html)
|
|
64
|
+
and [PostgreSQL formatting documentation](https://www.postgresql.org/docs/current/functions-formatting.html).
|
|
65
|
+
|
|
66
|
+
## Supported output zones
|
|
67
|
+
|
|
68
|
+
| Adapter | Zone behavior |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| PostgreSQL | Converts each stored UTC timestamp with the configured IANA zone, including historical offsets and daylight saving. Rails names such as `Eastern Time (US & Canada)` are converted to their IANA identifiers. The session time zone does not change the result. |
|
|
71
|
+
| SQLite | Uses the configured zone's current permanent offset, including fractional hours, as a fixed offset for every row. UTC, present-day Guatemala, Kolkata and Kathmandu are examples. A zone whose current TZInfo period has an upcoming transition raises `InvalidTimeZone`; New York is an example. |
|
|
72
|
+
|
|
73
|
+
SQLite cannot apply historical IANA rules. Its fixed-offset mode is intended for
|
|
74
|
+
records within the zone's current permanent-offset period. Earlier records,
|
|
75
|
+
such as Guatemala during its former daylight-saving years, require Ruby
|
|
76
|
+
presentation or PostgreSQL. A named zone accepted today could be rejected after
|
|
77
|
+
a tzdata update introduces a future transition. Prefer UTC for unrestricted
|
|
78
|
+
historical SQLite data. The formatter's input value does not select an offset
|
|
79
|
+
for individual database rows.
|
|
80
|
+
|
|
81
|
+
## Current database time
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
LesliDate::Compatibility.db_now
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Returns `CURRENT_TIMESTAMP` for SQLite and `NOW()` otherwise, preserving the
|
|
88
|
+
original two-line helper. Both are executed by the database and do not depend
|
|
89
|
+
on Ruby's clock. Column formatting supports only SQLite and PostgreSQL.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Application examples
|
|
2
|
+
|
|
3
|
+
These examples assume a Lesli application with this presentation configuration:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
config.datetime = {
|
|
7
|
+
time_zone: "America/Guatemala",
|
|
8
|
+
formats: { date: "%d/%m/%Y", date_time: "%d/%m/%Y %H:%M" }
|
|
9
|
+
}
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`Date2` can replace `LesliDate::Formatter` in applications that provide the usual
|
|
13
|
+
`class Date2 < LesliDate::Formatter` subclass.
|
|
14
|
+
|
|
15
|
+
## Date fields in forms
|
|
16
|
+
|
|
17
|
+
An HTML date field submits `YYYY-MM-DD`. Keep the resulting calendar date when
|
|
18
|
+
assigning a Rails `date` attribute:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# params[:birthday] == "2026-10-22"
|
|
22
|
+
person.birthday = LesliDate::Formatter.new(params[:birthday], "%Y-%m-%d").get
|
|
23
|
+
# => Date.new(2026, 10, 22)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use ISO output for the HTML field and the configured format for display:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
LesliDate::Formatter.new(person.birthday).get.iso8601 # => "2026-10-22"
|
|
30
|
+
LesliDate::Formatter.new(person.birthday).date.to_s # => "22/10/2026"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A `datetime-local` field has no offset. Specify its input zone when it represents
|
|
34
|
+
local time, then persist the instant in UTC:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# params[:starts_at] == "2026-10-22T08:30"
|
|
38
|
+
event.starts_at = LesliDate::Formatter.new(
|
|
39
|
+
params[:starts_at], "%Y-%m-%dT%H:%M", input_zone: "America/Guatemala"
|
|
40
|
+
).get.utc
|
|
41
|
+
# => 2026-10-22 14:30:00 UTC
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Handle `LesliDate::ParseError` in the application's validation flow. Native Ruby
|
|
45
|
+
parsing determines accepted input; an explicit format does not add strict
|
|
46
|
+
whole-string validation. See [Parsing rules](/gems/date/api/parsing).
|
|
47
|
+
|
|
48
|
+
## API timestamps
|
|
49
|
+
|
|
50
|
+
For an API timestamp with an explicit offset, retain the instant. Use ISO 8601
|
|
51
|
+
for the machine value and the configured presentation format for display:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
formatter = LesliDate::Formatter.new("2026-10-22T14:30:45.123Z")
|
|
55
|
+
|
|
56
|
+
render json: {
|
|
57
|
+
started_at: formatter.get.utc.iso8601(3),
|
|
58
|
+
started_at_display: formatter.date_time.to_s
|
|
59
|
+
}
|
|
60
|
+
# { "started_at": "2026-10-22T14:30:45.123Z",
|
|
61
|
+
# "started_at_display": "22/10/2026 08:30" }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For a calendar-date API field, use `.get.iso8601` to emit `YYYY-MM-DD`.
|
|
65
|
+
`locale:` controls display words only; it does not change input parsing or ISO
|
|
66
|
+
serialization. Missing or invalid inputs raise `ParseError`; the API controller
|
|
67
|
+
chooses the response status and error representation.
|
|
68
|
+
|
|
69
|
+
## Importing local timestamps
|
|
70
|
+
|
|
71
|
+
For a CSV that supplies local Guatemala times in `DD/MM/YYYY HH:MM`, declare
|
|
72
|
+
both the input pattern and its zone:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
require "csv"
|
|
76
|
+
|
|
77
|
+
CSV.foreach("events.csv", headers: true) do |row|
|
|
78
|
+
timestamp = LesliDate::Formatter.new(
|
|
79
|
+
row.fetch("occurred_at"), "%d/%m/%Y %H:%M", input_zone: "America/Guatemala"
|
|
80
|
+
).get
|
|
81
|
+
|
|
82
|
+
Event.create!(occurred_at: timestamp.utc)
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If the source already supplies UTC times, omit `input_zone:`. Explicit offsets
|
|
87
|
+
in the data take precedence over that option. Rescue `ParseError` per row if the
|
|
88
|
+
import should collect failures and continue; no fallback pattern is tried.
|
|
89
|
+
|
|
90
|
+
## Database presentation
|
|
91
|
+
|
|
92
|
+
Select display strings alongside the original UTC timestamps:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
formatter = LesliDate::Formatter.new.date_time
|
|
96
|
+
events = Event.select("events.*", formatter.db_timestamps("events"))
|
|
97
|
+
|
|
98
|
+
events.first.created_at_string # => "22/10/2026 08:30", for 14:30 UTC
|
|
99
|
+
events.first.updated_at_string
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Select one column with an explicit alias:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Event.select(:id, formatter.db_column(:occurred_at, "events", as: "occurred_at_display"))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The helpers generate SQL without executing a query or modifying their arguments.
|
|
109
|
+
Both adapters use `*_string` aliases. SQL output does not use Rails translations;
|
|
110
|
+
use `.to_s` for localized words. SQLite supports fixed offsets and its native
|
|
111
|
+
`strftime` directives; unsupported directives return `NULL`. PostgreSQL handles
|
|
112
|
+
IANA zones per row. See [Database expressions](/gems/date/api/database) for the
|
|
113
|
+
supported column types, directives and zone limits.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Formatter
|
|
2
|
+
|
|
3
|
+
`LesliDate::Formatter` interprets calendar dates and instants and exposes a set of display formats. Calendar dates retain their day; instants are converted to the presentation time zone.
|
|
4
|
+
|
|
5
|
+
## Create a formatter
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
formatter = LesliDate::Formatter.new(Time.current)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The constructor accepts a value, an optional positional input format, zone options, and an optional output locale:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
LesliDate::Formatter.new("2026/10/05 14:30", "%Y/%m/%d %H:%M")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Argument | Default | Purpose |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `datetime` | `Time.current` | Date or instant to interpret; explicit `nil` is invalid |
|
|
20
|
+
| `format` | `nil` | Native Ruby `strptime` pattern; otherwise use `DateTime.parse` |
|
|
21
|
+
| `input_zone:` | `"UTC"` | Interpret date-time strings that have no offset |
|
|
22
|
+
| `zone:` | `nil` | Presentation zone; `nil` reads `Lesli.config.datetime[:time_zone]` |
|
|
23
|
+
| `locale:` | `nil` | Output language; `nil` uses `I18n.locale` when rendering |
|
|
24
|
+
|
|
25
|
+
Lesli's database timestamps are UTC. The configured zone controls output;
|
|
26
|
+
normal Lesli calls need no zone keywords. `input_zone:` is available for
|
|
27
|
+
external sources that provide local times instead of UTC.
|
|
28
|
+
|
|
29
|
+
Accepted objects are `Date`, `DateTime`, `Time`, and `ActiveSupport::TimeWithZone`.
|
|
30
|
+
Strings are interpreted with Ruby's `DateTime.parse` or, when a pattern is
|
|
31
|
+
provided, `DateTime.strptime`. Accepted syntax and directives follow Ruby rather
|
|
32
|
+
than a separate LesliDate format language. See [Parsing rules](/gems/date/api/parsing)
|
|
33
|
+
for timezone behavior and errors.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
LesliDate::Formatter.new(
|
|
37
|
+
"2026-10-22 14:30",
|
|
38
|
+
input_zone: "America/Guatemala",
|
|
39
|
+
zone: "UTC"
|
|
40
|
+
).date_time.to_s
|
|
41
|
+
# => "22.10.2026 20:30", with output format %d.%m.%Y %H:%M
|
|
42
|
+
|
|
43
|
+
LesliDate::Formatter.new("2026-10-22").get
|
|
44
|
+
# => Date.new(2026, 10, 22)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Select an output format
|
|
48
|
+
|
|
49
|
+
Every selector uses its matching key in `Lesli.config.datetime[:formats]`.
|
|
50
|
+
Missing or `nil` values use these fallback patterns:
|
|
51
|
+
|
|
52
|
+
| Method | Fallback pattern |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `date` | `%d.%m.%Y` |
|
|
55
|
+
| `time` | `%H:%M` |
|
|
56
|
+
| `date_time` | `%d.%m.%Y %H:%M` |
|
|
57
|
+
| `date_words` | `%B %d, %Y` |
|
|
58
|
+
| `date_words_day` | `%A, %B %d, %Y` |
|
|
59
|
+
| `date_time_words` | `%B %d, %Y, %H:%M` |
|
|
60
|
+
| `date_time_words_pm` | `%B %d, %Y, %I:%M %p` |
|
|
61
|
+
| `date_time_words_day` | `%A, %B %d, %Y, %H:%M` |
|
|
62
|
+
|
|
63
|
+
Each selector returns the formatter:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
formatter.date_time_words.to_s
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Selectors change the output format of the same instance. They do not parse the
|
|
70
|
+
input again or replace `.get`. Input strings, temporal values and the host's
|
|
71
|
+
configuration are not modified. Patterns are copied at construction, so later
|
|
72
|
+
configuration edits do not change an existing formatter. `.get` exposes the
|
|
73
|
+
underlying parsed value without cloning it. Use a separate formatter for
|
|
74
|
+
independent selector state.
|
|
75
|
+
|
|
76
|
+
`to_s` uses `I18n.localize` with the selected pattern. Days, months and AM/PM use
|
|
77
|
+
the translations already loaded by Rails. LesliDate does not install or replace
|
|
78
|
+
translation data. The initializer determines the pattern and the current Rails
|
|
79
|
+
locale determines its translated words:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
I18n.with_locale(:es) do
|
|
83
|
+
LesliDate::Formatter.new("2026-10-22").date_words.to_s
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
LesliDate::Formatter.new("2026-10-22", locale: :es).date_words.to_s
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`get` returns a `Date` for calendar inputs
|
|
90
|
+
and an `ActiveSupport::TimeWithZone` for instants. Selecting `.date` on an instant
|
|
91
|
+
does not discard its underlying time or fractional seconds.
|
|
92
|
+
|
|
93
|
+
Calendar dates support `date`, `date_words`, and `date_words_day`. Selecting
|
|
94
|
+
`time` or any `date_time*` format raises `LesliDate::InvalidFormat` without
|
|
95
|
+
changing the previous output format.
|
|
96
|
+
|
|
97
|
+
Invalid values and native parsing failures raise `LesliDate::ParseError`;
|
|
98
|
+
invalid zone options raise `LesliDate::InvalidTimeZone`. `InvalidFormat` is used
|
|
99
|
+
for invalid output configuration and time selectors applied to calendar dates. All inherit from
|
|
100
|
+
`LesliDate::Error < ArgumentError`.
|
|
101
|
+
|
|
102
|
+
See [Configuration](/gems/date/about/configuration) for defaults, validation,
|
|
103
|
+
locales and optional translation overrides in Lesli.
|
|
104
|
+
See [Application examples](/gems/date/api/examples) for forms, APIs and imports.
|
data/docs/api/index
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# API overview
|
|
2
|
+
|
|
3
|
+
LesliDate has two public entry points:
|
|
4
|
+
|
|
5
|
+
| API | Purpose |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `LesliDate::Formatter` | Parse a date or time, apply the configured time zone, format it for display, or generate a formatted SQL selection |
|
|
8
|
+
| `LesliDate::Compatibility.db_now` | Return the current-timestamp SQL expression for the active database adapter |
|
|
9
|
+
|
|
10
|
+
A formatter is mutable: calling `date`, `time`, or another format selector changes the output format and returns the same formatter instance.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
formatter = LesliDate::Formatter.new(Time.current)
|
|
14
|
+
|
|
15
|
+
formatter.date.to_s
|
|
16
|
+
formatter.date_time_words.to_s
|
|
17
|
+
formatter.get
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`get` returns a `Date` for calendar inputs and an `ActiveSupport::TimeWithZone`
|
|
21
|
+
for instants. Calendar dates support only date selectors; time selectors raise
|
|
22
|
+
`LesliDate::InvalidFormat`.
|
|
23
|
+
|
|
24
|
+
Use [Parsing rules](/gems/date/api/parsing) for accepted inputs, zones and errors,
|
|
25
|
+
[Formatter](/gems/date/api/formatter) for display values, and
|
|
26
|
+
[Database expressions](/gems/date/api/database) for SQL selection helpers.
|