# Gettime
[](https://hex.pm/packages/gettime)
[](https://hexdocs.pm/gettime)
[](https://opensource.org/licenses/MIT)
A powerful and flexible Elixir library for converting database timestamps to user-specific timezones with configurable formatting. Inspired by the simplicity of `gettext`, Gettime provides seamless timezone conversion for Phoenix applications and APIs.
## Why Gettime?
- **Multiple Input Formats**: Handles `DateTime`, `NaiveDateTime`, Unix timestamps, ISO8601, RFC3339, and common date strings
- **Configurable Defaults**: Set application-wide timezone and format preferences
- **Custom Format Support**: Add your own timestamp parsing patterns via config or at runtime
- **Batch Processing**: Convert multiple timestamps efficiently in one operation
- **Zero External APIs**: Uses Elixir's built-in `tzdata` - works offline
- **Phoenix Integration**: Designed for Phoenix controllers, LiveView, and contexts
- **Comprehensive Error Handling**: Clear error messages for debugging
## Installation
Add `gettime` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:gettime, "~> 0.1.0"}
]
end
```
Then run:
```bash
mix deps.get
```
## Configuration
Add to your `config/config.exs`:
```elixir
config :gettime,
default_db_timezone: "UTC", # Your database timezone
default_user_timezone: "America/New_York", # Default user timezone
default_format: "%Y-%m-%d %H:%M:%S %Z", # Default output format
# Optional: Custom input formats for parsing unusual timestamp strings
custom_input_formats: [
{~r/^(\d{4})\.(\d{2})\.(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/, :parse_dot_format},
{~r/^(\d{2})-(\d{2})-(\d{4})\s+(\d{2}):(\d{2}):(\d{2})$/, :parse_dmy_format}
]
```
## Quick Start
```elixir
# Basic conversion with defaults
{:ok, result} = Gettime.convert(~N[2024-01-15 14:30:00])
# => {:ok, "2024-01-15 09:30:00 EST"}
# Convert to specific timezone
{:ok, result} = Gettime.convert(~N[2024-01-15 14:30:00], "Europe/London")
# => {:ok, "2024-01-15 14:30:00 GMT"}
# Custom format
{:ok, result} = Gettime.convert(
~N[2024-01-15 14:30:00],
"Asia/Tokyo",
"%B %d, %Y at %I:%M %p"
)
# => {:ok, "January 15, 2024 at 11:30 PM"}
```
## Supported Input Formats
Gettime automatically detects and parses these timestamp formats:
### Native Elixir Types
- **`DateTime`** - `%DateTime{}`
- **`NaiveDateTime`** - `~N[2024-01-15 14:30:00]`
- **`Date`** - `~D[2024-01-15]` (converts to midnight)
### Unix Timestamps
- **Integer** - `1705330200`
- **Float** - `1705330200.123`
### ISO Standards
- **ISO8601 with timezone** - `"2024-01-15T14:30:00Z"`
- **ISO8601 with offset** - `"2024-01-15T14:30:00+00:00"`
- **ISO8601 without timezone** - `"2024-01-15T14:30:00"`
- **Date only** - `"2024-01-15"`
### Common Date Strings
- **Standard format** - `"2024-01-15 14:30:00"`
- **US format** - `"01/15/2024 14:30:00"`
- **EU format** - `"15/01/2024 14:30:00"`
## API Reference
### Core Functions
#### `convert/3`
Converts a single timestamp to the target timezone.
```elixir
@spec convert(timestamp, timezone | nil, format | nil) :: {:ok, String.t()} | {:error, term}
```
**Parameters:**
- `timestamp` - Any supported timestamp format
- `timezone` - Target timezone (optional, uses config default)
- `format` - Output format string (optional, uses config default)
**Examples:**
```elixir
# Different input types
Gettime.convert(~N[2024-01-15 14:30:00], "Europe/Paris")
Gettime.convert("2024-01-15T14:30:00Z", "Asia/Tokyo")
Gettime.convert(1705330200, "America/Los_Angeles")
# Custom formatting
Gettime.convert(~N[2024-01-15 14:30:00], "UTC", "%Y-%m-%d")
# => {:ok, "2024-01-15"}
Gettime.convert(~N[2024-01-15 14:30:00], "America/New_York", "%B %d, %Y at %I:%M %p %Z")
# => {:ok, "January 15, 2024 at 09:30 AM EST"}
```
#### `convert_batch/3`
Efficiently converts multiple timestamps at once.
```elixir
@spec convert_batch([timestamp], timezone | nil, format | nil) :: {:ok, [String.t()]} | {:error, term}
```
**Example:**
```elixir
timestamps = [
~N[2024-01-15 14:30:00],
"2024-01-15T15:45:00Z",
1705334100
]
{:ok, results} = Gettime.convert_batch(timestamps, "America/Chicago")
# => {:ok, ["2024-01-15 08:30:00 CST", "2024-01-15 09:45:00 CST", "2024-01-15 09:55:00 CST"]}
```
### Utility Functions
#### `available_timezones/0`
Returns list of all available timezone identifiers.
```elixir
timezones = Gettime.available_timezones()
# => ["Africa/Abidjan", "Africa/Accra", ...]
```
#### `valid_timezone?/1`
Validates if a timezone identifier is valid.
```elixir
Gettime.valid_timezone?("America/New_York") # => true
Gettime.valid_timezone?("Invalid/Zone") # => false
```
### Custom Format Support
#### `add_custom_format/2`
Add custom timestamp parsing patterns at runtime.
```elixir
# Parser function that returns {:ok, DateTime/NaiveDateTime/Date} or {:error, reason}
custom_parser = fn timestamp_string, regex ->
case Regex.run(regex, timestamp_string) do
[_, year, month, day] ->
Date.new(String.to_integer(year), String.to_integer(month), String.to_integer(day))
_ ->
{:error, :invalid_format}
end
end
# Add the custom format
Gettime.add_custom_format(~r/^(\d{4})\|(\d{2})\|(\d{2})$/, custom_parser)
# Now you can use it
{:ok, result} = Gettime.convert("2024|01|15", "UTC")
# => {:ok, "2024-01-15 00:00:00 UTC"}
```
## Output Format Strings
Gettime uses Elixir's `Calendar.strftime/2` for formatting. Common patterns:
| Pattern | Description | Example |
|---------|-------------|---------|
| `%Y` | 4-digit year | `2024` |
| `%m` | Month (01-12) | `01` |
| `%d` | Day (01-31) | `15` |
| `%H` | Hour 24-hour (00-23) | `14` |
| `%I` | Hour 12-hour (01-12) | `02` |
| `%M` | Minute (00-59) | `30` |
| `%S` | Second (00-59) | `00` |
| `%p` | AM/PM | `PM` |
| `%Z` | Timezone abbreviation | `EST` |
| `%B` | Full month name | `January` |
| `%b` | Abbreviated month | `Jan` |
**Common format examples:**
```elixir
# US format
"%m/%d/%Y %I:%M %p" # => "01/15/2024 09:30 AM"
# ISO format
"%Y-%m-%dT%H:%M:%S%z" # => "2024-01-15T09:30:00-0500"
# Readable format
"%B %d, %Y at %I:%M %p %Z" # => "January 15, 2024 at 09:30 AM EST"
# Date only
"%Y-%m-%d" # => "2024-01-15"
```
## Phoenix Integration
### Controllers
```elixir
defmodule MyAppWeb.PostController do
use MyAppWeb, :controller
def index(conn, _params) do
posts = Blog.list_posts()
user_timezone = get_user_timezone(conn)
formatted_posts = Enum.map(posts, fn post ->
{:ok, display_date} = Gettime.convert(post.inserted_at, user_timezone, "%B %d, %Y")
Map.put(post, :display_date, display_date)
end)
render(conn, :index, posts: formatted_posts)
end
defp get_user_timezone(conn) do
# Get from user session, preferences, or IP geolocation
get_session(conn, :timezone) || "UTC"
end
end
```
### LiveView
```elixir
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
def mount(_params, session, socket) do
user_timezone = session["user_timezone"] || "UTC"
current_time_str = case Gettime.convert(DateTime.utc_now(), user_timezone) do
{:ok, time} -> time
{:error, _} -> "Invalid timezone"
end
socket =
socket
|> assign(:user_timezone, user_timezone)
|> assign(:current_time, current_time_str)
{:ok, socket}
end
def handle_event("change_timezone", %{"timezone" => tz}, socket) do
if Gettime.valid_timezone?(tz) do
{:ok, current_time} = Gettime.convert(DateTime.utc_now(), tz)
socket =
socket
|> assign(:user_timezone, tz)
|> assign(:current_time, current_time)
{:noreply, socket}
else
{:noreply, put_flash(socket, :error, "Invalid timezone")}
end
end
end
```
## Error Handling
Gettime provides detailed error information for debugging:
```elixir
case Gettime.convert("invalid-timestamp", "UTC") do
{:ok, result} ->
result
{:error, {:unparseable_timestamp, timestamp}} ->
"Could not parse: #{timestamp}"
{:error, {:invalid_timezone, tz}} ->
"Invalid timezone: #{tz}"
{:error, {:datetime_conversion_failed, reason}} ->
"Conversion failed: #{inspect(reason)}"
end
```
**Common error types:**
- `{:unparseable_timestamp, string}` - Input string couldn't be parsed
- `{:invalid_timezone, timezone}` - Invalid timezone identifier
- `{:datetime_conversion_failed, reason}` - Timezone conversion failed
- `{:formatting_failed, error}` - Output formatting failed
- `:unsupported_timestamp_format` - Input type not supported
## Performance Considerations
### Batch Operations
Use `convert_batch/3` for multiple timestamps - it's more efficient than individual conversions:
```elixir
# Good - single batch operation
{:ok, results} = Gettime.convert_batch(timestamps, timezone)
# Less efficient - multiple individual calls
results = Enum.map(timestamps, &Gettime.convert(&1, timezone))
```
### Timezone Validation
Cache timezone validation results when processing many records:
```elixir
def convert_with_cached_validation(timestamp, timezone) do
if valid_timezone_cache(timezone) do
Gettime.convert(timestamp, timezone)
else
{:error, {:invalid_timezone, timezone}}
end
end
```
### Default Configuration
Set sensible application defaults to minimize parameter passing:
```elixir
# Configure once
config :gettime,
default_user_timezone: get_app_default_timezone(),
default_format: get_app_default_format()
# Use throughout app
Gettime.convert(timestamp) # Uses configured defaults
```
## Testing
Test timezone conversions in your applications:
```elixir
defmodule MyAppTest do
use ExUnit.Case
test "converts user timestamps correctly" do
# Test with known timestamp and timezone
input = ~N[2024-01-15 14:30:00]
assert {:ok, "2024-01-15 09:30:00 EST"} =
Gettime.convert(input, "America/New_York")
assert {:ok, "2024-01-15 23:30:00 JST"} =
Gettime.convert(input, "Asia/Tokyo")
end
test "handles invalid inputs gracefully" do
assert {:error, {:unparseable_timestamp, "invalid"}} =
Gettime.convert("invalid", "UTC")
assert {:error, {:invalid_timezone, "Invalid/Zone"}} =
Gettime.convert(~N[2024-01-15 14:30:00], "Invalid/Zone")
end
end
```
## Troubleshooting
### Debug Mode
Enable debug logging to troubleshoot parsing issues:
```elixir
# In config/dev.exs
config :logger, level: :debug
# Check what format is being attempted
case Gettime.convert("unusual-format", "UTC") do
{:error, {:unparseable_timestamp, input}} ->
IO.puts("Could not parse: #{inspect(input)}")
# Try adding a custom format for this pattern
end
```
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b my-new-feature`)
3. Make your changes with tests
4. Run the test suite (`mix test`)
5. Run the formatter (`mix format`)
6. Submit a pull request
## License
MIT License. See [LICENSE](LICENSE) for details.
## Changelog
### v0.1.0
- Initial release
- Support for multiple timestamp formats
- Configurable defaults
- Custom format support
- Batch conversion
- Comprehensive error handling