Laravel Readable
Laravel Readable turns raw values into text people can read: numbers with thousands separators or K/M/B suffixes, numbers in words, dates, times, date differences, durations and file sizes. Every formatter is available as a static method, a global helper and a Blade directive. Number grouping, spelling and byte sizes come from PHP Readable, and dates and durations from Carbon, so they follow your application locale.
Readable Numbers
10203040 becomes 10,203,040, and 77700 becomes 77K or 77.7K.
Numbers in Words
7721 becomes seven thousand seven hundred twenty-one, in English or any intl language.
Dates and Times
Print 24 April 2020, 03:20 PM or a full date and time, in any timezone.
Date Differences
Print 29 years before, or every unit between two dates.
Durations
7777777 seconds becomes 3 months 29 minutes 37 seconds, or 3mos 29m 37s.
File Sizes
1500 bytes becomes 1.5 KB, and 7168 bytes becomes 7 KiB.
Directives, helpers and methods share one implementation. @readableFileSize($bytes), readable_file_size($bytes) and Readable::getFileSize($bytes) always print the same thing.
#Installation
Install the package with Composer. Laravel discovers the service provider and the Readable alias automatically.
#Requirements
- PHP 8.0, 8.1 or 8.2
- Laravel 9.33 or newer within 9.x
pharaonic/php-readable8.0.3+ on PHP 8.0, 8.1.2+ on PHP 8.1 or 8.2.2+ on PHP 8.2 (installed automatically)ext-intl, only to spell numbers in a language other than English
#Composer Installation
composer require pharaonic/laravel-readableThere is no configuration to publish and no migration to run.
You're all set! Try @readableNumber(10203040) in any Blade view.
#Upgrading from 1.x
Every 1.x method, helper, Blade directive and the Readable alias still work with the same signatures, and every correct 1.x output is unchanged. The upgrade fixes values that 1.x got wrong and introduces clearer names; the 1.x names stay as deprecated aliases.
#Requirements
6.x requires PHP 8.0 and Laravel 6.20+. The package now depends on illuminate/support, illuminate/view and pharaonic/php-readable instead of laravel/framework.
#Renamed APIs
Six methods have clearer names, helpers are now snake_case, and Blade directives are now camelCase. The 1.x names call the new ones and return the same values.
| 1.x method | 6.x method |
|---|---|
getHumanNumber() | getCompactNumber() |
getNumberToString() | getNumberInWords() |
getDecInt() | getDecimalOrInteger() |
getDiffDateTime() | getRelativeDateTime() |
getTimeLength() | getDuration() |
getSize() | getFileSize() |
Helpers and directives follow the method names:
| 1.x helper | 6.x helper | 1.x directive | 6.x directive |
|---|---|---|---|
ReadableNumber() | readable_number() | @ReadableNumber | @readableNumber |
ReadableHumanNumber() | readable_compact_number() | @ReadableHumanNumber | @readableCompactNumber |
ReadableNumberToString() | readable_number_in_words() | @ReadableNumberToString | @readableNumberInWords |
ReadableDecimal() | readable_decimal() | @ReadableDecimal | @readableDecimal |
ReadableDecInt() | readable_decimal_or_integer() | @ReadableDecInt | @readableDecimalOrInteger |
ReadableDate() | readable_date() | @ReadableDate | @readableDate |
ReadableTime() | readable_time() | @ReadableTime | @readableTime |
ReadableDateTime() | readable_date_time() | @ReadableDateTime | @readableDateTime |
ReadableDiffDateTime() | readable_relative_date_time() | @ReadableDiffDateTime | @readableRelativeDateTime |
ReadableTimeLength() | readable_duration() | @ReadableTimeLength | @readableDuration |
ReadableDateTimeLength() | readable_date_time_length() | @ReadableDateTimeLength | @readableDateTimeLength |
ReadableSize() | readable_file_size() | @ReadableSize | @readableFileSize |
The new methods also use clearer parameter names, which matters only with named arguments: $locale instead of $lang, $decimals instead of $decimals_length, $separator instead of $comma, and $seconds instead of $input in getDuration(). The deprecated names keep their 1.x parameter names.
The 1.x names are deprecated. They keep working on every current line and are removed in the first line that supports PHP 8.6. Move to the new names when you upgrade.
#Fixed Values
The table uses the 1.x names; the new names return the same values.
| Call | 1.x | 6.x |
|---|---|---|
getNumber(PHP_INT_MAX) | 9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
getHumanNumber(999999, true) | 1,000.0K | 1.0M |
getHumanNumber(-1500) | ErrorException | -1K |
getDecInt(69.999) | 70.00 | 70 |
getNumberToString(21, 'ar_EG') | واحد و عشرون | واحد وعشرون |
getTimeLength(0) | 1 second | 0 seconds |
getTimeLength(31536000) | 1 year 5 days | 1 year |
getTimeLength(-60) | 11 months 4 weeks 1 day 23 hours 59 minutes | 1 minute |
getSize(0) | null | 0 B |
getSize(999999) | 1000 KB | 1 MB |
getSize(4865) | 4.86 KB | 4.87 KB |
getSize(10 ** 15) | ErrorException | 1 PB |
Passing a Carbon instance with a timezone no longer changes your instance:
1$date = Carbon::parse('2020-04-24 23:30:00', 'UTC');2 3Readable::getDate($date, 'Asia/Riyadh'); // "25 April 2020"4$date->tzName; // "UTC" (1.x: "Asia/Riyadh")#Other Changes
getNumberInWords()spells English without the intl extension. Other languages throwMissingIntlExtensionwhen intl is missing.getNumberInWords(INF)andgetNumberInWords(NAN)throwInvalidArgumentException(1.x printedinfinityandnot a number).getDuration()counts a year as 365 days (it was 360). A month is still 30 days.- Binary sizes between 1000 and 1023 units are grouped:
1,000 KiB. Readable::prepareDateTime()also returns the prepared instance.- Global helpers are declared only when a function with the same name doesn't already exist.
getCompactNumber() still truncates without decimals (77700 → 77K), getFileSize() still returns null for negative sizes, and Blade directives still echo without escaping.
#Basic Usage
Each formatter can be called three ways, with the same arguments and the same result.
1use Pharaonic\Laravel\Readable\Readable;2 3Readable::getFileSize(1500); // "1.5 KB"4readable_file_size(1500); // "1.5 KB"1@readableFileSize(1500) {{-- 1.5 KB --}}The Readable alias works too, without an import:
1\Readable::getNumber(10203040); // "10,203,040"#Naming
| Static method | Helper | Blade directive |
|---|---|---|
Readable::getNumber() | readable_number() | @readableNumber() |
Readable::getCompactNumber() | readable_compact_number() | @readableCompactNumber() |
Readable::getNumberInWords() | readable_number_in_words() | @readableNumberInWords() |
Readable::getDecimal() | readable_decimal() | @readableDecimal() |
Readable::getDecimalOrInteger() | readable_decimal_or_integer() | @readableDecimalOrInteger() |
Readable::getDate() | readable_date() | @readableDate() |
Readable::getTime() | readable_time() | @readableTime() |
Readable::getDateTime() | readable_date_time() | @readableDateTime() |
Readable::getRelativeDateTime() | readable_relative_date_time() | @readableRelativeDateTime() |
Readable::getDuration() | readable_duration() | @readableDuration() |
Readable::getDateTimeLength() | readable_date_time_length() | @readableDateTimeLength() |
Readable::getFileSize() | readable_file_size() | @readableFileSize() |
#Numbers
#Grouped Integers
getNumber() groups an integer's thousands. The second argument changes the delimiter.
1Readable::getNumber(10203040); // "10,203,040"2Readable::getNumber(-1234567); // "-1,234,567"3Readable::getNumber(1234567, ' '); // "1 234 567"4Readable::getNumber(1234567, '.'); // "1.234.567"5Readable::getNumber(PHP_INT_MAX); // "9,223,372,036,854,775,807"#Short Numbers
getCompactNumber() shortens a number with a K, M, B or T suffix.
1Readable::getCompactNumber(999); // "999"2Readable::getCompactNumber(77700); // "77K"3Readable::getCompactNumber(77700, true); // "77.7K"4Readable::getCompactNumber(77749, true, 2); // "77.75K"5Readable::getCompactNumber(1250000, true); // "1.3M"6Readable::getCompactNumber(999999, true); // "1.0M"7Readable::getCompactNumber(-1500); // "-1K"Without $showDecimal, the value is truncated to whole units (1500 → 1K). With it, the value is rounded to $decimals places (1 by default) and moves to the next unit when rounding reaches 1000. Values past 999T keep the T suffix.
$decimals always prints that many places, even when they are zeros: getCompactNumber(1999999999, true, 2) is 2.00B.
#Decimals
getDecimal() formats a number with a fixed number of decimals, rounding half up. You can change the decimal point and the thousands delimiter.
1Readable::getDecimal(60708.546); // "60,708.55"2Readable::getDecimal(7); // "7.00"3Readable::getDecimal(1234.5678, 3, ',', '.'); // "1.234,568"#Decimals Only When Needed
getDecimalOrInteger() prints integers, and floats that round to a whole number, without decimals.
1Readable::getDecimalOrInteger(70.0); // "70"2Readable::getDecimalOrInteger(69.999); // "70"3Readable::getDecimalOrInteger(70.07); // "70.07"4Readable::getDecimalOrInteger(-3.5); // "-3.50"5Readable::getDecimalOrInteger(70, 3); // "70"It takes the same $point and $delimiter arguments as getDecimal().
#Numbers in Words
getNumberInWords() spells a number out. English is spelled natively. Any other language uses the intl extension.
1Readable::getNumberInWords(7721); // "seven thousand seven hundred twenty-one"2Readable::getNumberInWords(-15); // "minus fifteen"3Readable::getNumberInWords(1.5); // "one point five"4Readable::getNumberInWords(21, 'fr'); // "vingt-et-un"5Readable::getNumberInWords(21, 'ar'); // "واحد وعشرون"getDecimal(), getDecimalOrInteger() and getNumberInWords() accept only real integers and floats. A numeric string such as '7' throws TypeError('Wrong Input Type.'), so cast database strings first: readable_decimal((float) $price).
#Dates & Times
Date methods accept anything Carbon::parse() understands: date strings, Unix timestamps, DateTime and Carbon instances. null means now. The optional $timezone converts the date before formatting, without changing your instance.
#Date
1Readable::getDate('24-04-2020'); // "24 April 2020"2Readable::getDate($post->created_at); // "24 April 2020"3Readable::getDate('2020-04-24 23:30:00', 'Asia/Riyadh'); // "25 April 2020"#Time
getTime($input, $is12 = false, $hasSeconds = false, $timezone = null):
1Readable::getTime('15:20:22'); // "15:20"2Readable::getTime('15:20:22', false, true); // "15:20:22"3Readable::getTime('15:20:22', true); // "03:20 PM"4Readable::getTime('15:20:22', true, true); // "03:20:22 PM"5Readable::getTime('2020-01-01 15:20', false, false, 'Africa/Cairo'); // "17:20"#Date and Time
getDateTime() takes the same arguments as getTime():
1Readable::getDateTime('24-04-2020 17:20:30'); // "Friday, April 24, 2020 17:20"2Readable::getDateTime('24-04-2020 17:20:30', true); // "Friday, April 24, 2020 05:20 PM"3Readable::getDateTime('24-04-2020 17:20:30', true, true); // "Friday, April 24, 2020 05:20:30 PM"#Difference
getRelativeDateTime($date, $other = null, $timezone = null) prints the largest unit between two dates. Without $other, it compares with now.
1Readable::getRelativeDateTime('01-02-1993 19:00:00', '01-02-2022 19:00:00'); // "29 years before"2Readable::getRelativeDateTime('2022-01-01', '1993-01-01'); // "29 years after"3Readable::getRelativeDateTime(now()->subHour()); // "1 hour before"#Full Difference
getDateTimeLength($old, $new = null, $full = false, $comma = ' ', $timezone = null) prints every unit when $full is true, joined by $comma.
1Readable::getDateTimeLength('1993-01-02 19:00:00', '2022-03-05 20:07:07');2// "29 years before"3 4Readable::getDateTimeLength('1993-01-02 19:00:00', '2022-03-05 20:07:07', true);5// "29 years 2 months 3 days 1 hour 7 minutes 7 seconds before"6 7Readable::getDateTimeLength('1993-01-02 19:00:00', '2022-03-05 20:07:07', true, ' - ');8// "29 years - 2 months - 3 days - 1 hour - 7 minutes - 7 seconds before"Month names, day names, AM/PM and words such as years and before come from Carbon, which follows app()->setLocale().
#Durations
getDuration($seconds, $separator = ' ', $short = false) turns a number of seconds into a duration.
1Readable::getDuration(3661); // "1 hour 1 minute 1 second"2Readable::getDuration(7777777); // "3 months 29 minutes 37 seconds"3Readable::getDuration(7777777, ', '); // "3 months, 29 minutes, 37 seconds"4Readable::getDuration(7777777, ' ', true); // "3mos 29m 37s"5Readable::getDuration(1558800); // "2 weeks 4 days 1 hour"6Readable::getDuration(0); // "0 seconds"A year is 365 days and a month is 30 days. Units with a zero value are skipped, and the sign is ignored (-60 is 1 minute).
To measure the time between two dates with calendar months and years, use getDateTimeLength() instead.
#File Sizes
getFileSize($bytes, $decimal = true) formats a byte count with up to 2 decimals, dropping trailing zeros.
1Readable::getFileSize(1500); // "1.5 KB"2Readable::getFileSize(123456789); // "123.46 MB"3Readable::getFileSize(999999); // "1 MB"4Readable::getFileSize(0); // "0 B"5Readable::getFileSize(-1); // nullBy default 1 KB is 1000 bytes. Pass false for binary units, where 1 KiB is 1024 bytes:
1Readable::getFileSize(7168, false); // "7 KiB"2Readable::getFileSize(123456789, false); // "117.74 MiB"3Readable::getFileSize(1048576, false); // "1 MiB"| Mode | Units |
|---|---|
| Decimal (default) | B, KB, MB, GB, TB, PB, EB, ZB, YB |
| Binary | B, KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB |
The value moves to the next unit when rounding reaches it, so 999999 is 1 MB, not 1000 KB.
#Helpers
Every static method except prepareDateTime() has a snake_case global helper with the same arguments. Helpers are loaded through Composer, so they work anywhere: controllers, jobs, mail, Blade {{ }} echoes.
1public function toMail($notifiable)2{3 return (new MailMessage)4 ->line('Your export is ready: '.readable_file_size($this->export->size).'.')5 ->line('It contains '.readable_number($this->export->rows).' rows.');6}| Helper | Same as |
|---|---|
readable_number(int $input, string $delimiter = ',') | Readable::getNumber() |
readable_compact_number(int $input, bool $showDecimal = false, int $decimals = 0) | Readable::getCompactNumber() |
readable_number_in_words($input, string $locale = 'en') | Readable::getNumberInWords() |
readable_decimal($input, int $decimals = 2, string $point = '.', string $delimiter = ',') | Readable::getDecimal() |
readable_decimal_or_integer($input, int $decimals = 2, string $point = '.', string $delimiter = ',') | Readable::getDecimalOrInteger() |
readable_date($input, ?string $timezone = null) | Readable::getDate() |
readable_time($input, bool $is12 = false, bool $hasSeconds = false, ?string $timezone = null) | Readable::getTime() |
readable_date_time($input, bool $is12 = false, bool $hasSeconds = false, ?string $timezone = null) | Readable::getDateTime() |
readable_relative_date_time($date, $other = null, ?string $timezone = null) | Readable::getRelativeDateTime() |
readable_duration(int $seconds, string $separator = ' ', bool $short = false) | Readable::getDuration() |
readable_date_time_length($old, $new = null, bool $full = false, string $comma = ' ', ?string $timezone = null) | Readable::getDateTimeLength() |
readable_file_size(int $bytes, bool $decimal = true) | Readable::getFileSize() |
Each helper is declared only if no function with that name exists yet, so a helper you define first wins.
#Blade Directives
Every formatter has a directive that takes the method's arguments and echoes the result.
1<article>2 <h1>{{ $post->title }}</h1>3 <p>4 @readableDate($post->published_at)5 · @readableCompactNumber($post->views, true) views6 · @readableDuration($post->reading_seconds) read7 </p>8</article>Directives work inline with text:
1<span>Size: @readableFileSize($file->size).</span> {{-- Size: 1.5 KB. --}}The available directives are @readableNumber, @readableCompactNumber, @readableNumberInWords, @readableDecimal, @readableDecimalOrInteger, @readableDate, @readableTime, @readableDateTime, @readableRelativeDateTime, @readableDuration, @readableDateTimeLength and @readableFileSize.
Directives echo the result without e(), like {!! !!}. That lets you use an entity as a delimiter, such as @readableNumber($n, ' '), but never pass user input as a delimiter or decimal point. Use {{ readable_number($n, $delimiter) }} when you need escaping.
@readableFileSize(-1) prints nothing, because getFileSize() returns null for negative sizes.
#API Reference
All methods are static on Pharaonic\Laravel\Readable\Readable, also available as the Readable alias.
#Numbers
| Method | Description | Returns |
|---|---|---|
getNumber(int $input, string $delimiter = ',') | Integer with grouped thousands. | string |
getCompactNumber(int $input, bool $showDecimal = false, int $decimals = 0) | Number with a K, M, B or T suffix. Truncated without $showDecimal, rounded with it. | string |
getNumberInWords(int|float $input, string $locale = 'en') | Number in words. Non-English locales need intl. | string |
getDecimal(int|float $input, int $decimals = 2, string $point = '.', string $delimiter = ',') | Number with fixed decimals. | string |
getDecimalOrInteger(int|float $input, int $decimals = 2, string $point = '.', string $delimiter = ',') | Like getDecimal(), without decimals for whole values. | string |
#Dates & Durations
| Method | Description | Returns |
|---|---|---|
getDate($input, ?string $timezone = null) | 24 April 2020 | string |
getTime($input, $is12 = false, bool $hasSeconds = false, ?string $timezone = null) | 15:20, 03:20:22 PM | string |
getDateTime($input, $is12 = false, bool $hasSeconds = false, ?string $timezone = null) | Friday, April 24, 2020 05:20 PM | string |
getRelativeDateTime($date, $other = null, ?string $timezone = null) | 29 years before. $other defaults to now. | string |
getDateTimeLength($old, $new = null, bool $full = false, string $comma = ' ', ?string $timezone = null) | Every unit between two dates when $full. | string |
getDuration(int $seconds, string $separator = ' ', bool $short = false) | Seconds as a duration. | string |
prepareDateTime(&$input, ?string $tz = null) | Parses $input into a Carbon instance in place, converted to $tz. Used by every date method. | Carbon |
#File Sizes
| Method | Description | Returns |
|---|---|---|
getFileSize(int $bytes, bool $decimal = true) | 1.5 KB (1000-based) or 1.46 KiB (1024-based). | string, or null for negative sizes |
#Exceptions
| Exception | Thrown by | When |
|---|---|---|
TypeError('Wrong Input Type.') | getNumberInWords(), getDecimal(), getDecimalOrInteger() | The input is not an int or a float. |
InvalidArgumentException | getNumberInWords() | The input is INF or NAN, or (on newer ICU builds) $locale is not a valid locale. |
Pharaonic\Readable\Exceptions\MissingIntlExtension | getNumberInWords() | A non-English locale without the intl extension. |
InvalidArgumentException (Carbon's InvalidFormatException on recent Carbon 2 releases) | Date methods | Carbon can't parse the input. |
#Examples
#1. File Manager
Show each upload's size, age and owner-friendly date.
1namespace App\Http\Controllers; 2 3use App\Models\File; 4 5class FileController extends Controller 6{ 7 public function index() 8 { 9 return view('files.index', [10 'files' => File::latest()->paginate(20),11 'used' => File::sum('size'),12 ]);13 }14} 1<p>Storage used: @readableFileSize((int) $used)</p> 2 3@foreach ($files as $file) 4 <tr> 5 <td>{{ $file->name }}</td> 6 <td>@readableFileSize($file->size)</td> 7 <td title="@readableDateTime($file->created_at, true)"> 8 @readableRelativeDateTime($file->created_at) 9 </td>10 </tr>11@endforeach#2. Social Counters
Truncated counters never overstate: 1999 followers is 1K, not 2K.
1<ul>2 <li>@readableCompactNumber($user->followers_count) followers</li>3 <li>@readableCompactNumber($user->posts_count) posts</li>4 <li>@readableCompactNumber($user->likes_count, true) likes</li>5</ul>#3. Invoice Amounts in Words
Print the total in figures and in words, in the customer's language.
1public function getTotalInWordsAttribute(): string2{3 return readable_number_in_words((float) $this->total, $this->customer->locale);4}5 6public function getFormattedTotalAttribute(): string7{8 return readable_decimal((float) $this->total).' '.$this->currency;9}Customers with a non-English locale need the intl extension on the server.
#4. Video Lengths and Uptime
1readable_duration($video->duration_seconds, ' ', true); // "1h 4m 12s"2readable_duration($server->uptime, ', '); // "3 days, 4 hours, 12 minutes"#5. Times in the Viewer's Timezone
Store UTC and show each user their own time.
1<p>2 Starts @readableDate($event->starts_at, auth()->user()->timezone)3 at @readableTime($event->starts_at, true, false, auth()->user()->timezone)4</p>$event->starts_at is not changed, so later uses still see UTC.
#Troubleshooting
#TypeError: Wrong Input Type.
getDecimal(), getDecimalOrInteger() and getNumberInWords() accept only int and float. Database columns such as decimal come back as strings, so cast them: readable_decimal((float) $order->total).
#MissingIntlExtension when spelling a number
You passed a language other than en to getNumberInWords() and the intl extension isn't installed. Install ext-intl, or use English.
#Dates are in English although my app is in another language
Carbon follows app()->setLocale(). Set the locale before rendering, for example in a middleware, and make sure Carbon has a translation for it.
#The time is off by a few hours
The input is parsed in the application timezone (config/app.php → timezone). Pass the viewer's timezone as the last argument, such as readable_time($date, true, false, 'Africa/Cairo').
#@readableDate throws Too few arguments
The directive needs at least one argument. Use @readableDate(null) for today's date.
#@readableFileSize prints nothing
The size is negative. getFileSize() returns null for negative sizes, and 0 prints 0 B.
#A helper does something different
Another package or your app declared a function with the same name first, for example readable_file_size(). Call the static method instead: Readable::getFileSize().
Contributors
Amazing people who made this package possible
This package wouldn't exist without the contributions of our amazing community. A huge thank you to everyone who has helped improve Readable!
Want to Contribute?
We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.