Current section
Files
Jump to
Current section
Files
lib/file_size.ex
defmodule FileSize do
@moduledoc """
A file size calculator, parser and formatter.
## Usage
You can build your own file size by creating it with a number and a unit using
the `new/2` function. See the "Supported Units" section for a list of possible
unit atoms.
iex> FileSize.new(16, :gb)
#FileSize<"16.0 GB">
### Sigil
There is also a sigil defined that you can use to quickly build file sizes
from a number and unit symbol. Import the `FileSize.Sigil` module and you are
ready to go. See the "Supported Units" section for a list of possible unit
symbols.
iex> import FileSize.Sigil
...>
...> ~F(16 GB)
#FileSize<"16.0 GB">
### From File
With `from_file/1` it is also possible to retrieve the size of an actual file.
iex> FileSize.from_file("path/to/my/file.txt")
{:ok, #FileSize<"127.3 kB">}
### Conversions
You can convert file sizes between different units or unit systems by using
the `convert/2` function.
### Calculations
You can calculate with file sizes. The particular units don't need to be the
same for that.
* `add/2` - Add two file sizes.
* `subtract/2` - Subtracts two file sizes.
### Comparison
For comparison the units of the particular file sizes don't need to be the
same.
* `compare/2` - Compares two file sizes and returns a value indicating whether
one file size is greater than or less than the other.
* `equals?/2` - Determines whether two file sizes are equal.
* `lt?/2` - Determines whether file size a < b.
* `lte?/2` - Determines whether file size a <= b.
* `gt?/2` - Determines whether file size a > b.
* `gte?/2` - Determines whether file size a >= b.
To sort a collection of file sizes from smallest to greatest, you can use
`lte?/2` as sort function. To sort descending use `gte?/2`.
iex> sizes = [~F(16 GB), ~F(100 Mbit), ~F(27.4 MB), ~F(16 Gbit)]
...> Enum.sort(sizes, &FileSize.lte?/2)
[#FileSize<"100.0 Mbit">, #FileSize<"27.4 MB">, #FileSize<"16.0 Gbit">, #FileSize<"16.0 GB">]
## Supported Units
### Bit-based
#### SI (Système international d'unités)
| Atom | Symbol | Name | Factor |
|----------|--------|------------|--------|
| `:bit` | bit | Bits | 1 |
| `:kbit` | kbit | Kilobits | 1000 |
| `:mbit` | Mbit | Megabits | 1000^2 |
| `:gbit` | GBit | Gigabits | 1000^3 |
| `:tbit` | TBit | Terabits | 1000^4 |
| `:pbit` | PBit | Petabits | 1000^5 |
| `:ebit` | EBit | Exabits | 1000^6 |
| `:zbit` | ZBit | Zetabits | 1000^7 |
| `:ybit` | YBit | Yottabits | 1000^8 |
#### IEC (International Electrotechnical Commission)
| Atom | Symbol | Name | Factor |
|----------|--------|------------|--------|
| `:bit` | Bit | Bits | 1 |
| `:kibit` | Kibit | Kibibits | 1024 |
| `:mibit` | Mibit | Mebibits | 1024^2 |
| `:gibit` | Gibit | Gibibits | 1024^3 |
| `:tibit` | Tibit | Tebibits | 1024^4 |
| `:pibit` | Pibit | Pebibits | 1024^5 |
| `:eibit` | Eibit | Exbibits | 1024^6 |
| `:zibit` | Zibit | Zebibits | 1024^7 |
| `:yibit` | Yibit | Yobibits | 1024^8 |
### Byte-based
The most common unit of digital information. A single Byte represents 8 Bits.
#### SI (Système international d'unités)
| Atom | Symbol | Name | Factor |
|----------|--------|------------|--------|
| `:b` | B | Bytes | 1 |
| `:kb` | kB | Kilobytes | 1000 |
| `:mb` | MB | Megabytes | 1000^2 |
| `:gb` | GB | Gigabytes | 1000^3 |
| `:tb` | TB | Terabytes | 1000^4 |
| `:pb` | PB | Petabytes | 1000^5 |
| `:eb` | EB | Exabytes | 1000^6 |
| `:zb` | ZB | Zetabytes | 1000^7 |
| `:yb` | YB | Yottabytes | 1000^8 |
#### IEC (International Electrotechnical Commission)
| Atom | Symbol | Name | Factor |
|----------|--------|------------|--------|
| `:b` | B | Bytes | 1 |
| `:kib` | KiB | Kibibytes | 1024 |
| `:mib` | MiB | Mebibytes | 1024^2 |
| `:gib` | GiB | Gibibytes | 1024^3 |
| `:tib` | TiB | Tebibytes | 1024^4 |
| `:pib` | PiB | Pebibytes | 1024^5 |
| `:eib` | EiB | Exbibytes | 1024^6 |
| `:zib` | ZiB | Zebibytes | 1024^7 |
| `:yib` | YiB | Yobibytes | 1024^8 |
"""
alias FileSize.Bit
alias FileSize.Byte
alias FileSize.Calculable
alias FileSize.Comparable
alias FileSize.Convertible
alias FileSize.Formatter
alias FileSize.Parser
alias FileSize.Units
alias FileSize.Units.Info, as: UnitInfo
@typedoc """
A type that defines the IEC bit and byte units.
"""
@type iec_unit :: Bit.iec_unit() | Byte.iec_unit()
@typedoc """
A type that defines the SI bit and byte units.
"""
@type si_unit :: Bit.si_unit() | Byte.si_unit()
@typedoc """
A type that is a union of the bit and byte types.
"""
@type t :: Bit.t() | Byte.t()
@typedoc """
A type that is a union of the bit and byte unit types and
`t:FileSize.Units.Info.t/0`.
"""
@type unit :: iec_unit | si_unit | UnitInfo.t()
@typedoc """
A type that represents a unit symbol.
"""
@type unit_symbol :: String.t()
@typedoc """
A type that contains the available unit systems.
"""
@type unit_system :: :iec | :si
@typedoc """
A type that defines the value used to create a new file size.
"""
@type value :: number | Decimal.t() | String.t()
@doc false
defmacro __using__(_) do
quote do
import FileSize.Sigil
end
end
@doc """
Gets the configuration.
"""
@spec __config__() :: Keyword.t()
def __config__ do
Application.get_all_env(:file_size)
end
@doc """
Builds a new file size. Raises when the given unit could not be found.
## Examples
iex> FileSize.new(2.5, :mb)
#FileSize<"2.5 MB">
iex> FileSize.new(214, :kib)
#FileSize<"214 KiB">
iex> FileSize.new(3, :bit)
#FileSize<"3 bit">
"""
@spec new(value, unit) :: t | no_return
def new(value, unit_or_unit_info \\ :b)
def new(value, %UnitInfo{mod: mod} = unit_info) do
mod.new(value, unit_info)
end
def new(value, unit) do
new(value, Units.fetch!(unit))
end
@doc """
Builds a new file size from the given number of bits.
## Example
iex> FileSize.from_bytes(2000)
#FileSize<"2000 B">
"""
def from_bytes(bytes), do: FileSize.new(bytes, :b)
@doc """
Builds a new file size from the given number of bits, allowing conversion
in the same step.
## Options
When a keyword list is given, you must specify one of the following options.
* `:convert` - Converts the file size to the given `t:unit/0`.
* `:scale` - Scales and converts the file size to an appropriate unit in the
specified `t:unit_system/0`.
## Examples
iex> FileSize.from_bytes(2000, scale: :iec)
#FileSize<"1.953125 KiB">
iex> FileSize.from_bytes(16, scale: :unknown)
** (FileSize.InvalidUnitSystemError) Invalid unit system: :unknown
iex> FileSize.from_bytes(2, convert: :bit)
#FileSize<"16 bit">
iex> FileSize.from_bytes(1600, :kb)
#FileSize<"1.6 kB">
iex> FileSize.from_bytes(16, convert: :unknown)
** (FileSize.InvalidUnitError) Invalid unit: :unknown
"""
@spec from_bytes(value, unit | Keyword.t()) :: t
def from_bytes(bytes, unit_or_unit_info_or_opts)
def from_bytes(bytes, opts) when is_list(opts) do
do_from_bytes(bytes, opts)
end
def from_bytes(bytes, unit_or_unit_info) do
do_from_bytes(bytes, convert: unit_or_unit_info)
end
defp do_from_bytes(bytes, convert: unit_or_unit_info) do
bytes
|> from_bytes()
|> convert(unit_or_unit_info)
end
defp do_from_bytes(bytes, scale: unit_system) do
bytes
|> from_bytes()
|> scale(unit_system)
end
@doc """
Builds a new file size from the given number of bits.
## Example
iex> FileSize.from_bits(2000)
#FileSize<"2000 bit">
"""
def from_bits(bits), do: FileSize.new(bits, :bit)
@doc """
Builds a new file size from the given number of bits, allowing conversion
in the same step.
## Options
When a keyword list is given, you must specify one of the following options.
* `:convert` - Converts the file size to the given `t:unit/0`.
* `:scale` - Scales and converts the file size to an appropriate unit in the
specified `t:unit_system/0`.
## Examples
iex> FileSize.from_bits(2000, scale: :iec)
#FileSize<"1.953125 Kibit">
iex> FileSize.from_bits(16, scale: :unknown)
** (FileSize.InvalidUnitSystemError) Invalid unit system: :unknown
iex> FileSize.from_bits(16, convert: :b)
#FileSize<"2 B">
iex> FileSize.from_bits(1600, :kbit)
#FileSize<"1.6 kbit">
iex> FileSize.from_bits(16, convert: :unknown)
** (FileSize.InvalidUnitError) Invalid unit: :unknown
"""
@spec from_bits(value, unit | Keyword.t()) :: t
def from_bits(bits, unit_or_unit_info_or_opts)
def from_bits(bits, opts) when is_list(opts) do
do_from_bits(bits, opts)
end
def from_bits(bits, unit_or_unit_info) do
do_from_bits(bits, convert: unit_or_unit_info)
end
defp do_from_bits(bits, convert: unit_or_unit_info) do
bits
|> from_bits()
|> convert(unit_or_unit_info)
end
defp do_from_bits(bits, scale: unit_system) do
bits
|> from_bits()
|> scale(unit_system)
end
@doc """
Determines the size of the file at the given path.
## Options
When a keyword list is given, you must specify one of the following options.
* `:convert` - Converts the file size to the given `t:unit/0`.
* `:scale` - Scales and converts the file size to an appropriate unit in the
specified `t:unit_system/0`.
## Examples
iex> FileSize.from_file("path/to/my/file.txt")
{:ok, #FileSize<"133.7 kB">}
iex> FileSize.from_file("path/to/my/file.txt", :mb)
{:ok, #FileSize<"0.13 MB">}
iex> FileSize.from_file("path/to/my/file.txt", unit: :mb)
{:ok, #FileSize<"0.13 MB">}
iex> FileSize.from_file("path/to/my/file.txt", scale: :iec)
{:ok, #FileSize<"133.7 KiB">}
iex> FileSize.from_file("not/existing/file.txt")
{:error, :enoent}
"""
@spec from_file(Path.t(), unit | Keyword.t()) ::
{:ok, t} | {:error, File.posix()}
def from_file(path, unit_or_unit_info_or_opts \\ :b) do
with {:ok, %{size: value}} <- File.stat(path) do
{:ok, from_bytes(value, unit_or_unit_info_or_opts)}
end
end
@doc """
Determines the size of the file at the given path. Raises when the file could
not be found.
## Options
When a keyword list is given, you must specify one of the following options.
* `:convert` - Converts the file size to the given `t:unit/0`.
* `:scale` - Scales and converts the file size to an appropriate unit in the
specified `t:unit_system/0`.
## Examples
iex> FileSize.from_file!("path/to/my/file.txt")
#FileSize<"133.7 kB">
iex> FileSize.from_file!("path/to/my/file.txt", :mb)
#FileSize<"0.13 MB">
iex> FileSize.from_file!("path/to/my/file.txt", unit: :mb)
#FileSize<"0.13 MB">
iex> FileSize.from_file!("path/to/my/file.txt", system: :iec)
#FileSize<"133.7 KiB">
iex> FileSize.from_file!("not/existing/file.txt")
** (File.Error) could not read file stats "not/existing/file.txt": no such file or directory
"""
@spec from_file!(Path.t(), unit | Keyword.t()) ::
t | no_return
def from_file!(path, unit_or_unit_info_or_opts \\ :b) do
path
|> File.stat!()
|> Map.fetch!(:size)
|> from_bytes(unit_or_unit_info_or_opts)
end
defdelegate parse(value), to: Parser
defdelegate parse!(value), to: Parser
defdelegate format(size, opts \\ []), to: Formatter
@doc """
Converts the given file size to a given unit or unit system.
## Options
When a keyword list is given, you must specify one of the following options.
* `:unit` - Converts the file size to the given `t:unit/0`.
* `:system` - Converts the file size to the given `t:unit_system/0`.
## Examples
iex> FileSize.convert(FileSize.new(2, :kb), :b)
#FileSize<"2000 B">
iex> FileSize.convert(FileSize.new(2000, :b), unit: :kb)
#FileSize<"2 kB">
iex> FileSize.convert(FileSize.new(20, :kb), :kbit)
#FileSize<"160 kbit">
iex> FileSize.convert(FileSize.new(2, :kb), system: :iec)
#FileSize<"1.953125 KiB">
iex> FileSize.convert(FileSize.new(2, :kib), system: :si)
#FileSize<"2.048 kB">
iex> FileSize.convert(FileSize.new(2000, :b), unit: :unknown)
** (FileSize.InvalidUnitError) Invalid unit: :unknown
iex> FileSize.convert(FileSize.new(2, :b), system: :unknown)
** (FileSize.InvalidUnitSystemError) Invalid unit system: :unknown
"""
@spec convert(t, unit | Keyword.t()) :: t
def convert(size, unit_or_unit_info_or_opts)
def convert(size, opts) when is_list(opts) do
do_convert(size, opts)
end
def convert(size, unit_or_unit_info) do
do_convert(size, unit: unit_or_unit_info)
end
defp do_convert(size, unit: unit) do
Convertible.convert(size, Units.fetch!(unit))
end
defp do_convert(size, system: unit_system) do
convert(size, Units.equivalent_unit_for_system!(size.unit, unit_system))
end
@doc """
Converts the given file size to the most appropriate unit. When no unit system
is specified, the unit system of the source file size is used. If no unit
system could be inferred from the size, the SI unit system is used.
## Examples
iex> FileSize.scale(FileSize.new(2000, :b))
#FileSize<"2 kB">
iex> FileSize.scale(FileSize.new(2_000_000, :kb))
#FileSize<"2 GB">
iex> FileSize.scale(FileSize.new(2_000_000, :kb), :iec)
#FileSize<"1.86264514923095703125 GiB">
iex> FileSize.scale(FileSize.new(2000, :b), :unknown)
** (FileSize.InvalidUnitSystemError) Invalid unit system: :unknown
"""
@doc since: "1.1.0"
@spec scale(t, nil | unit_system) :: t
def scale(size, unit_system \\ nil) do
convert(size, Units.appropriate_unit_for_size!(size, unit_system))
end
defdelegate compare(size, other_size), to: Comparable
@doc """
Determines whether two file sizes are equal.
## Examples
iex> FileSize.equals?(FileSize.new(2, :b), FileSize.new(16, :bit))
true
iex> FileSize.equals?(FileSize.new(2, :b), FileSize.new(2, :b))
true
iex> FileSize.equals?(FileSize.new(1, :b), FileSize.new(2, :b))
false
"""
@spec equals?(t, t) :: boolean
def equals?(size, other_size) do
compare(size, other_size) == 0
end
@doc """
Determines whether the first file size is less than the second one.
## Examples
iex> FileSize.lt?(FileSize.new(1, :b), FileSize.new(2, :b))
true
iex> FileSize.lt?(FileSize.new(2, :b), FileSize.new(1, :b))
false
"""
@doc since: "1.2.0"
@spec lt?(t, t) :: boolean
def lt?(size, other_size) do
compare(size, other_size) == -1
end
@doc """
Determines whether the first file size is less or equal to than the second
one.
## Examples
iex> FileSize.lte?(FileSize.new(1, :b), FileSize.new(2, :b))
true
iex> FileSize.lte?(FileSize.new(1, :b), FileSize.new(1, :b))
true
iex> FileSize.lte?(FileSize.new(2, :b), FileSize.new(1, :b))
false
"""
@doc since: "2.0.0"
@spec lte?(t, t) :: boolean
def lte?(size, other_size) do
compare(size, other_size) <= 0
end
@doc """
Determines whether the first file size is less or equal to than the second
one.
"""
@doc since: "1.2.0"
@deprecated "Use lte?/2 instead"
@spec lteq?(t, t) :: boolean
def lteq?(size, other_size) do
lte?(size, other_size)
end
@doc """
Determines whether the first file size is greater than the second one.
## Examples
iex> FileSize.gt?(FileSize.new(2, :b), FileSize.new(1, :b))
true
iex> FileSize.gt?(FileSize.new(1, :b), FileSize.new(2, :b))
false
"""
@doc since: "1.2.0"
@spec gt?(t, t) :: boolean
def gt?(size, other_size) do
compare(size, other_size) == 1
end
@doc """
Determines whether the first file size is less or equal to than the second
one.
## Examples
iex> FileSize.gte?(FileSize.new(2, :b), FileSize.new(1, :b))
true
iex> FileSize.gte?(FileSize.new(1, :b), FileSize.new(1, :b))
true
iex> FileSize.gte?(FileSize.new(1, :b), FileSize.new(2, :b))
false
"""
@doc since: "2.0.0"
@spec gte?(t, t) :: boolean
def gte?(size, other_size) do
compare(size, other_size) >= 0
end
@doc """
Determines whether the first file size is less or equal to than the second
one.
"""
@doc since: "1.2.0"
@deprecated "Use gte?/2 instead"
@spec gteq?(t, t) :: boolean
def gteq?(size, other_size) do
gte?(size, other_size)
end
defdelegate add(size, other_size), to: Calculable
@doc """
Adds two file sizes like `add/2` and converts the result to the specified
unit.
## Options
When a keyword list is given, you must specify one of the following options.
* `:unit` - Converts the file size to the given `t:unit/0`.
* `:system` - Converts the file size to the given `t:unit_system/0`.
## Examples
iex> FileSize.add(FileSize.new(1, :kb), FileSize.new(2, :kb), :b)
#FileSize<"3000 B">
iex> FileSize.add(FileSize.new(1, :kb), FileSize.new(2, :kb), unit: :b)
#FileSize<"3000 B">
iex> FileSize.add(FileSize.new(1, :kb), FileSize.new(2, :kb), system: :iec)
#FileSize<"2.9296875 KiB">
"""
@spec add(t, t, unit | Keyword.t()) :: t
def add(size, other_size, unit_or_unit_info_or_opts) do
size
|> add(other_size)
|> convert(unit_or_unit_info_or_opts)
end
defdelegate subtract(size, other_size), to: Calculable
@doc """
Subtracts two file sizes like `subtract/2` and converts the result to the
specified unit.
## Options
When a keyword list is given, you must specify one of the following options.
* `:unit` - Converts the file size to the given `t:unit/0`.
* `:system` - Converts the file size to the given `t:unit_system/0`.
## Examples
iex> FileSize.subtract(FileSize.new(2, :b), FileSize.new(6, :bit), :bit)
#FileSize<"10 bit">
iex> FileSize.subtract(FileSize.new(2, :b), FileSize.new(6, :bit), unit: :bit)
#FileSize<"10 bit">
iex> FileSize.subtract(FileSize.new(3, :kb), FileSize.new(1, :kb), system: :iec)
#FileSize<"1.953125 KiB">
"""
@spec subtract(t, t, unit | Keyword.t()) :: t
def subtract(size, other_size, unit_or_unit_info_or_opts) do
size
|> subtract(other_size)
|> convert(unit_or_unit_info_or_opts)
end
@doc """
Gets the normalized size from the given file size as integer.
## Example
iex> FileSize.to_integer(FileSize.new(2, :kbit))
2000
"""
@doc since: "2.0.0"
@spec to_integer(t) :: integer
def to_integer(size) do
size
|> Convertible.normalized_value()
|> Decimal.to_integer()
end
end