Laravel Packagev7.0.0MIT License

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.

Quick Tip

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
  • Laravel 7.30 or newer within 7.x
  • pharaonic/php-readable 8.0.3+ within 8.0.x (installed automatically)
  • ext-intl, only to spell numbers in a language other than English

#Composer Installation

Terminal
composer require pharaonic/laravel-readable

There is no configuration to publish and no migration to run.

Installation Complete

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 method6.x method
getHumanNumber()getCompactNumber()
getNumberToString()getNumberInWords()
getDecInt()getDecimalOrInteger()
getDiffDateTime()getRelativeDateTime()
getTimeLength()getDuration()
getSize()getFileSize()

Helpers and directives follow the method names:

1.x helper6.x helper1.x directive6.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.

Deprecated 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.

Call1.x6.x
getNumber(PHP_INT_MAX)9,223,372,036,854,775,8089,223,372,036,854,775,807
getHumanNumber(999999, true)1,000.0K1.0M
getHumanNumber(-1500)ErrorException-1K
getDecInt(69.999)70.0070
getNumberToString(21, 'ar_EG')واحد و عشرونواحد وعشرون
getTimeLength(0)1 second0 seconds
getTimeLength(31536000)1 year 5 days1 year
getTimeLength(-60)11 months 4 weeks 1 day 23 hours 59 minutes1 minute
getSize(0)null0 B
getSize(999999)1000 KB1 MB
getSize(4865)4.86 KB4.87 KB
getSize(10 ** 15)ErrorException1 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 throw MissingIntlExtension when intl is missing.
  • getNumberInWords(INF) and getNumberInWords(NAN) throw InvalidArgumentException (1.x printed infinity and not 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.
Unchanged on Purpose

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.

app/Http/Controllers/FileController.php
1use Pharaonic\Laravel\Readable\Readable;
2 
3Readable::getFileSize(1500); // "1.5 KB"
4readable_file_size(1500); // "1.5 KB"
resources/views/files/show.blade.php
1@readableFileSize(1500) {{-- 1.5 KB --}}

The Readable alias works too, without an import:

1\Readable::getNumber(10203040); // "10,203,040"

#Naming

Static methodHelperBlade 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.

Fixed Decimals

$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'); // "واحد وعشرون"
Strict Input

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"
Translations

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).

Between Two Dates

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); // null

By 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"
ModeUnits
Decimal (default)B, KB, MB, GB, TB, PB, EB, ZB, YB
BinaryB, 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.

app/Notifications/ExportReady.php
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}
HelperSame 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()
Name Clashes

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.

resources/views/posts/show.blade.php
1<article>
2 <h1>{{ $post->title }}</h1>
3 <p>
4 @readableDate($post->published_at)
5 · @readableCompactNumber($post->views, true) views
6 · @readableDuration($post->reading_seconds) read
7 </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.

Escaping

Directives echo the result without e(), like {!! !!}. That lets you use an entity as a delimiter, such as @readableNumber($n, '&nbsp;'), but never pass user input as a delimiter or decimal point. Use {{ readable_number($n, $delimiter) }} when you need escaping.

Null Output

@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

MethodDescriptionReturns
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

MethodDescriptionReturns
getDate($input, ?string $timezone = null)24 April 2020string
getTime($input, $is12 = false, bool $hasSeconds = false, ?string $timezone = null)15:20, 03:20:22 PMstring
getDateTime($input, $is12 = false, bool $hasSeconds = false, ?string $timezone = null)Friday, April 24, 2020 05:20 PMstring
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

MethodDescriptionReturns
getFileSize(int $bytes, bool $decimal = true)1.5 KB (1000-based) or 1.46 KiB (1024-based).string, or null for negative sizes

#Exceptions

ExceptionThrown byWhen
TypeError('Wrong Input Type.')getNumberInWords(), getDecimal(), getDecimalOrInteger()The input is not an int or a float.
InvalidArgumentExceptiongetNumberInWords()The input is INF or NAN, or (on newer ICU builds) $locale is not a valid locale.
Pharaonic\Readable\Exceptions\MissingIntlExtensiongetNumberInWords()A non-English locale without the intl extension.
InvalidArgumentException (Carbon's InvalidFormatException on recent Carbon 2 releases)Date methodsCarbon can't parse the input.

#Examples

#1. File Manager

Show each upload's size, age and owner-friendly date.

app/Http/Controllers/FileController.php
1namespace App\Http\Controllers;
2 
3use App\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}
resources/views/files/index.blade.php
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.

resources/views/profiles/show.blade.php
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.

app/Invoice.php
1public function getTotalInWordsAttribute(): string
2{
3 return readable_number_in_words((float) $this->total, $this->customer->locale);
4}
5 
6public function getFormattedTotalAttribute(): string
7{
8 return readable_decimal((float) $this->total).' '.$this->currency;
9}
intl

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.

resources/views/events/show.blade.php
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!

Moamen Eltouny

@MoamenEltouny

16 contributions

@denniskrol

1 contribution

Want to Contribute?

We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.