lesli_date 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98f6a82caee6c75ab6e3c9734827c809e3d1aa0240e914075915783498a5c390
4
- data.tar.gz: eb622a5530482c55ca05a6202783632450b2af3025264a1bd72b998cc6f8c389
3
+ metadata.gz: d64dec0455da16b095e0cb4b67fccb28fb2a3010f97dedcd97e0ac8178e272ca
4
+ data.tar.gz: 1452d2bf0482a4604b26b03341312e3ebab51fdedba716807fd38457cb11162d
5
5
  SHA512:
6
- metadata.gz: 6ac72c1eea1075046b372676f9de4b2c0fb8ebdc3ff9bc228ac557c295f0a87743d155092a08171c6bb9e4293983e8771ae20ee3a1c82ce7f9aa1142062e3db0
7
- data.tar.gz: 1a3707b14b70c9924b8ee9ee5145d0bad7a611cc21d23bd8c406c8e99204a9a1281e4df9420a579d5823459bea85b3d4985a2a9492580aad92d3d22525b88b9e
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
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
4
+ require "minitest/test_task"
5
5
 
6
- RSpec::Core::RakeTask.new(:spec)
6
+ Minitest::TestTask.create
7
7
 
8
- task default: :spec
8
+ task default: :test
@@ -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,6 @@
1
+ ---
2
+ title: API
3
+ ---
4
+
5
+ <!-- This section landing page introduces the supported formatter and database compatibility APIs. -->
6
+ <%= partial("overview") %>
@@ -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.