-
-
Save JonCatmull/ecdf9441aaa37336d9ae2c7f9cb7289a to your computer and use it in GitHub Desktop.
/** | |
* @license | |
* Copyright (c) 2019 Jonathan Catmull. | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
import { Pipe, PipeTransform } from '@angular/core'; | |
type unit = 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB'; | |
type unitPrecisionMap = { | |
[u in unit]: number; | |
}; | |
const defaultPrecisionMap: unitPrecisionMap = { | |
bytes: 0, | |
KB: 0, | |
MB: 1, | |
GB: 1, | |
TB: 2, | |
PB: 2 | |
}; | |
/* | |
* Convert bytes into largest possible unit. | |
* Takes an precision argument that can be a number or a map for each unit. | |
* Usage: | |
* bytes | fileSize:precision | |
* @example | |
* // returns 1 KB | |
* {{ 1500 | fileSize }} | |
* @example | |
* // returns 2.1 GB | |
* {{ 2100000000 | fileSize }} | |
* @example | |
* // returns 1.46 KB | |
* {{ 1500 | fileSize:2 }} | |
*/ | |
@Pipe({ name: 'fileSize' }) | |
export class FileSizePipe implements PipeTransform { | |
private readonly units: unit[] = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; | |
transform(bytes: number = 0, precision: number | unitPrecisionMap = defaultPrecisionMap): string { | |
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) return '?'; | |
let unitIndex = 0; | |
while (bytes >= 1024) { | |
bytes /= 1024; | |
unitIndex++; | |
} | |
const unit = this.units[unitIndex]; | |
if (typeof precision === 'number') { | |
return `${bytes.toFixed(+precision)} ${unit}`; | |
} | |
return `${bytes.toFixed(precision[unit])} ${unit}`; | |
} | |
} |
@bardiarastin Thanks for you comment, I have updated my code to fix the TypeScript compiler error you mentioned.
A simple suggestion. if (unit == 0) precision = 0;
after the while loop so you don't end up with "38.00 bytes."
I guess this wouldn't help if you were averaging file sizes or something but I bet total file size is it's more popular use.
Thanks
Still works like a charm, thanks for saving me the headache
just a minor note, KB might be interpreted as kilobyte which equals 1000 bytes, instead of the kibibyte (KiB) which equals 1024 bytes.
@JonCatmull this is a helpful filter! Would you mind putting a license header (MIT, Apache, ...) on it for reuse?
Excellent. Thank you!
Thank you!
Could someone explain the purpose of a bytes pipe to a junior dev? I'm finding code examples but not a good summary of when it would be needed. I understand what bytes and pipes are but I don't know the purpose of a bytes pipe. Appreciate any help.
Could someone explain the purpose of a bytes pipe to a junior dev?
- Bytes is the smallest unit you can have for a file. By using the smallest you are sure you can easily convert to all the upper ones
- Pipes allow to transform a data value to another "display" state in your templates (example: if your number is will a lot of decimals like 1.66666666666, you would probably prefer to round it for the user on the screen to 1.66; in that case pipes are usefull. There are of course more advantages, like changing the "display" data without changing the original value in your application logic if your pipe is pure and it should be), cf. https://angular.io/guide/pipes
- So using bytes as base unit you can show to the user a human readable value of a file on the user screen (that is the pipe job)
- Cherry on the top, if you want to format several bytes values accross your application, you can reuse your pipe everywhere in your templates (DRY, efficient, predictable, and easily testable if your pipe is pure), what could we ask more?
Another example would be dates. If you have different time formats of dates in your app logic it is often a nightmare and it is bug-prone. So in your application logic you stick to one date format (ISO, UTC, whatever) and on your user screen you use pipes like DatePipe to show the final "display" value of a date. Many formats for the user (pipes), but in the background one format to rule them all (ISO, UTC, whatever).
Thank you for this great piece of code 😊👍
I just optimized it a little bit for me by adding the standard decimal pipe to convert the number to local format, so it looks better on my German project 😉
https://gist.github.com/msteini82/6b77f92feda1f6f8286bc1bc7e6fd21b
Thanks, everyone for your comments! Sorry, I haven't been on here and the emails got overlooked.
@coreyog, @AarjavP, @donmccurdy, @msteini82
Thank you for your suggestions, I will definitely look to incorporate these and repost. Then perhaps this is worth putting in an npm module to easily import into your projects if anyone has a suggestion on the name then drop a comment below.
Thanks, again.
A simple suggestion.
if (unit == 0) precision = 0;
after the while loop so you don't end up with "38.00 bytes."I guess this wouldn't help if you were averaging file sizes or something but I bet total file size is it's more popular use.
And what about this one...
let unit = 0, division = bytes;
while ( division >= 1024 ) {
division /= 1024;
unit ++;
}
if (bytes % 1024 <= 10) precision = 0;
I found it cleaner to change:
while ( bytes >= 1024 ) {
to:
while ( bytes >= 1000 ) {
to avoid having something like 1021 kB. This would be rounded down to 1 MB which looks better.
Added a file size precision map so you can have more finite control over what precision you want per unit.
It still accepts a number for the same precision across all units.
There is a small issue if the bytes
is int value, will raise error bytes.toFixed is not a function
, a quick fix is convert to float before run toFixed
, bytes = parseFloat(bytes.toString());
import { Pipe, PipeTransform } from '@angular/core';
const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const FILE_SIZE_UNITS_LONG = ['Bytes', 'Kilobytes', 'Megabytes', 'Gigabytes', 'Pettabytes', 'Exabytes', 'Zettabytes', 'Yottabytes'];
@Pipe({
name: 'formatFileSize'
})
export class FormatFileSizePipe implements PipeTransform {
transform(sizeInBytes: number, longForm: boolean): string {
const units = longForm
? FILE_SIZE_UNITS_LONG
: FILE_SIZE_UNITS;
let power = Math.round(Math.log(sizeInBytes) / Math.log(1024));
power = Math.min(power, units.length - 1);
const size = sizeInBytes / Math.pow(1024, power); // size in new units
const formattedSize = Math.round(size * 100) / 100; // keep up to 2 decimals
const unit = units[power];
return `${formattedSize} ${unit}`;
}
}
}
nice, thanks
ERROR in [default] file-size.pipe.ts:16:28
Argument of type 'number' is not assignable to parameter of type 'string'.
since you are declaring bytes as a number , you can not pass it to parseFloat() so the code won't work .
the correct way is :
transform(bytes: number = 0, precision: number = 2 ) : string { if (!isFinite( bytes ) ){ return '?'; } let unit = 0; while ( bytes >= 1024 ) { bytes /= 1024; unit ++; }