# stamp: date formatting by example instead of by format string

> stamp is a small Ruby gem that replaces the strftime directive soup with a different question: not how do I write a format string, but what should the output look like. You hand it an example like March 1, 1999, and it reshapes June 9, 2011 to match, working out the rest, including ordinals, weekday names, time zone abbreviations and UTC offsets. It also offers a refinement and a module function for people who do not want Date and Time patched.

**jeremyw/stamp** — Date and time formatting for humans.

- Repository: https://github.com/jeremyw/stamp
- Stars: 960 · Forks: 17
- Language: Ruby
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/jeremyw-stamp

## A different question, and a different answer

The pitch of stamp is in its first two lines: format dates and times based on human-friendly examples, not arcane strftime directives.

That is a real change of model. The strftime approach asks you to encode a shape as a sequence of percent signs, and the result is a string that only one person at a time can read, if they are the person who wrote it. The stamp approach asks you to show what you want, and infers the shape from it.

```ruby
date = Date.new(2011, 6, 9)
date.stamp("March 1, 1999")         #=> "June 9, 2011"
date.stamp("Jan 1, 1999")           #=> "Jun 9, 2011"
date.stamp("Jan 01")                #=> "Jun 09"
date.stamp("Sunday, May 1, 2000")   #=> "Thursday, June 9, 2011"
date.stamp("Sun Aug 5")             #=> "Thu Jun 9"
date.stamp("12/31/99")              #=> "06/09/11"
date.stamp("DOB: 12/31/2000")       #=> "DOB: 06/09/2011"
```

Every one of those calls is readable by somebody who has never seen a strftime directive, and each shows both the input shape and the output, so a reader can infer the mapping without running anything. The last one is the clearest demonstration, since DOB: is arbitrary text that is carried through, and the surrounding date is reshaped to match the example.

The API is a method added to Date and Time, and the README is thoughtful about the objection that stamp is not a descriptive enough name for a developer reading the code later. If you want a different name, the aliases stamp_like and format_like are provided, and they are aliases for the same method rather than a separate implementation.

The scope is deliberately small. It is a formatting library, it does not parse, it does not do arithmetic, and it does not try to be a date library. It is the piece that turns a date object into the string a person wants to read.

## Ordinals, and the times half of the story

Two capabilities go beyond what a simple example format would suggest.

The first is ordinals. The README says it even formats ordinal days, and the examples are of the kind where a naive format string is genuinely awkward:

```ruby
date.stamp("November 5th")          #=> "June 9th"
date.stamp("1st of Jan")            #=> "9th of Jun"
```

The suffix follows the day number, so 1st becomes 9th and the month name is abbreviated according to the example. The second example also shows that the connector between the day and the month is part of the shape, so of is preserved.

The times half works the same way, and adds hours, minutes and seconds when it sees colon separated values:

```ruby
time = Time.utc(2011, 6, 9, 20, 52, 30)
time.stamp("3:00 AM")               #=> "8:52 PM"
time.stamp("01:00:00 AM")           #=> "08:52:30 PM"
time.stamp("23:59")                 #=> "20:52"
time.stamp("23:59:59")              #=> "20:52:30"
time.stamp("Jan 1 at 01:00 AM")     #=> "Jun 9 at 08:52 PM"
time.stamp("23:59 UTC")             #=> "20:52 PST"
```

Two things are worth reading carefully in that last line. The 12 hour with a meridiem, the 24 hour without one, and the presence or absence of seconds are all inferred from the example, so the same method covers four different conventions. And the zone abbreviation is recognised in the example, UTC.

The final example is the subtle one. The input is 23:59 UTC and the output is 20:52 PST, so the value is converted into the local zone and the local zone's abbreviation is used. The example is not merely a template, it participates in the interpretation, and a zone in the example changes what the output says. That is a design decision with consequences, because it means the same time formatted with two different examples gives two different zone labels.

UTC offsets are recognised as well, in both the +HHMM and the +HH:MM styles:

```ruby
time.stamp("23:59 +00:00")          #=> "20:52 -05:00"
```

so an offset in the example is turned into a numeric offset in the output, again with the conversion to the local zone applied.

## Three ways in, for people who dislike monkey patches

Adding a method to Date and Time is convenient and it is also a monkey patch, and the README offers two alternatives for people who would rather not have the standard library classes changed.

The first is a module function, which requires a different file:

```ruby
require "stamp/core" # instead of "stamp"

Stamp.format(Date.new(2011, 6, 9), "Jan 1, 1999")  #=> "Jun 9, 2011"
```

The second is a refinement, which is scoped to the files that opt in:

```ruby
require "stamp/refinement"
using Stamp::Refinement

Date.new(2011, 6, 9).stamp("Jan 1, 1999")          #=> "Jun 9, 2011"
```

A refinement is the better answer of the two in most codebases, because the effect is lexical rather than global. A file that says using Stamp::Refinement gets the method, and a file that does not is untouched, which is what you want in a library that other people will load. The module function has the same property but a clunkier call site.

The distinction matters more here than it would for a gem with one extra method, because the change lands on classes the whole process shares. A gem that adds stamp to Date and Time changes the behaviour of every gem in the process. The core and refinement entry points exist so that a project can take the feature without taking the global change, and the README makes the choice explicit rather than leaving it implicit.

The cost is that requiring the right file is your job. A file that forgets using Stamp::Refinement gets a NoMethodError rather than a wrong date, which is the good kind of failure.

## Turning an example back into a strftime string

One feature exists for the case where a format string is genuinely required, and it is the one that makes stamp usable in existing configurations rather than only in new code.

Stamp.strftime_format converts an example into the equivalent strftime directive string:

```ruby
Stamp.strftime_format("Jan 1, 1999")  #=> "%b %-d, %Y"
Stamp.strftime_format("23:59:59")     #=> "%H:%M:%S"
```

That matters for three places the README names. Time::DATE_FORMATS and the equivalent Date constants, which is how Rails and other frameworks attach a named format to a class. I18n configurations, where a translation file holds a format rather than a formatted string. And APIs that want a format rather than a value, such as a service that takes a pattern as a parameter and formats the date on its own side.

The limitation is spelled out precisely: ordinal days, for example 1st, have no strftime equivalent and raise an ArgumentError. That is the right behaviour, since there is no correct directive to return, and raising is better than returning a format that silently drops the suffix. It is also the one shape in the library that cannot be round-tripped, which is worth knowing if you are planning to store examples and convert them later.

The result is a library that does not demand you change your architecture. You can adopt it for new call sites that want the readability, and use strftime_format where the surrounding system insists on a pattern, and the two will agree.

## Disambiguation, and the argument for readable formats

The features list is short. Abbreviated and full names of months and weekdays are recognised. Days with or without a leading zero work. Standard time zone abbreviations are recognised, with UTC, PST and EST given as examples. And any extraneous text can be included, with DOB: as the example.

The feature that deserves more attention is disambiguation by value. The README explains that you can use any month, weekday, day or year value that makes sense in your examples, and that stamp can often infer your intent based on context, but there may be times when you need unambiguous values to make your intent explicit. The example given is 01/09, which could refer to January 9, September 1, or January 2009, and the more explicit alternatives are 12/31, 31/12 and 12/99.

That last one is the clever part. By using a day of 99 in the example, the shape of the format is fixed without committing to a month, because 99 cannot be a valid day, and the result is that the example can express a two digit year format without implying anything about the month.

The second half of that section is the argument that ties the library together. Using unambiguous values will also help people who read the code in the future, including yourself, understand your intent.

That is the case for the whole library in one sentence. A strftime directive is compact and unreadable, so the person who wrote it does not read it carefully either, and the cost of a wrong format is paid by whoever maintains it later. An example string is long and obvious, and it is unambiguous when you take the trouble to make it so.

The Rails integration is the last piece. Date and Time formats are configured with procs that call stamp:

```ruby
# config/initializers/date_formats.rb
Date::DATE_FORMATS[:short]    = Proc.new { |date| date.stamp("Sun Jan 5") }
Time::DATE_FORMATS[:military] = Proc.new { |time| time.stamp("5 January 23:59") }
```

and the formats are then used through the usual to_s with a symbol:

```ruby
Date.today.to_s(:short)   #=> "Sat Jul 16"
Time.now.to_s(:military)  #=> "16 July 15:35"
```

Note the two examples: one uses abbreviated month and weekday names and the other uses a full month name and a 24 hour clock, and the same machinery produces both, because the shape comes entirely from the example.

## Limitations, licence and the shape of the project

The limitations section is short and honest. DateTime should inherit stamp behaviour from Date, but it has not been thoroughly tested, and patches are welcome. That is the only gap the README admits, and it is a real one.

The copyright notice covers 2011 to 2026 and names Jeremy Weiskotten, with the terms in LICENSE.txt. The repository is MIT licensed according to the project metadata, and the README points to the licence file for the details. This describes the published terms rather than legal advice.

The repository is a conventional Ruby gem, with a stamp.gemspec, a Gemfile, a Rakefile, a lib directory, a test directory and a.yardopts file for the generated documentation, plus a.github directory with the CI workflow that the badge at the top of the README refers to. There are no GitHub releases attached to the repository, so the version history lives in the gem registry and in the changelog that a gem of this age would keep, and the last push was on 2026-08-02.

The judgement is easy to make. This is a small library that solves one annoying problem in a way that makes the code better, and it is careful about the two things that make a formatting library annoying in practice, which is that it patches core classes and that it makes you learn a new format language. It answers the first with a core entry point and a refinement, and the second by using a language you already know.

The remaining risk is the one every date formatting library carries, which is that the output depends on the local time zone, as the 23:59 UTC example shows. A format that looks right on one machine and differs on another is a classic source of confusing bugs, and stamp's behaviour of converting to the local zone and labelling the output with the local zone's abbreviation makes that visible rather than hiding it, which is at least the better of the two behaviours.

## Conclusion

Adopt stamp when a codebase is full of unreadable strftime strings, especially one where a human has to guess what a date will look like, because the example format is self-documenting by construction and can be lifted straight from the output. It is the wrong tool when you need a format that a fixed example cannot express, since every shape is derived from one sample string, and it is the wrong tool for DateTime, which the README says has not been thoroughly tested. Before relying on it, remember the disambiguation rule, because 01/09 is ambiguous between January 9, September 1 and January 2009, and use unambiguous values such as 12/31 where the meaning matters.

## FAQ

### What does the stamp gem do?

It formats dates and times by example rather than by strftime directive. You give it a sample string such as March 1, 1999 and the value is reshaped to match it, including abbreviated or full month names, weekdays, ordinals, leading zeros and any surrounding text.

### How do I use stamp without monkey patching Date and Time?

Require stamp/core and call the module function Stamp.format(date, example), or require stamp/refinement and add using Stamp::Refinement in the files that want the method. The refinement scopes the change to those files rather than to the whole process.

### Can stamp produce a strftime format string?

Yes. Stamp.strftime_format converts an example into the equivalent strftime string, for example Jan 1, 1999 becomes %b %-d, %Y. That is what you use for DATE_FORMATS, I18n configuration and APIs that want a format rather than a value. Ordinal days have no strftime equivalent and raise an ArgumentError.

### How do I avoid an ambiguous example such as 01/09?

Use values that cannot be confused, and the README suggests 12/31, 31/12 and 12/99, where 99 is not a valid day and therefore fixes the shape of the format without committing to a month or a year.

### What are the limitations of the stamp gem?

The README lists one: DateTime should inherit stamp behaviour from Date, but that has not been thoroughly tested, and patches are welcome. It also notes that a time zone in the example changes the output, since the value is converted to the local zone.

## Sources

- [Issues](https://github.com/jeremyw/stamp/issues)
- [jeremyw/stamp on GitHub](https://github.com/jeremyw/stamp)
- [License: MIT](https://github.com/jeremyw/stamp/blob/master/LICENSE)
- [README](https://github.com/jeremyw/stamp/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jeremyw-stamp
