diff --git a/.github/workflows/datasize.yml b/.github/workflows/datasize.yml index 1f46ec3..620e96a 100644 --- a/.github/workflows/datasize.yml +++ b/.github/workflows/datasize.yml @@ -2,9 +2,9 @@ name: datasize on: push: - branches: ["main"] + branches: ["main", "release/**"] pull_request: - branches: ["main", "feat/**"] + branches: ["main", "feat/**", "release/**"] jobs: build: diff --git a/README.md b/README.md index 6b1a223..862d586 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,6 @@ Use extension function to create the `DataSize` representation. It's important t `bytes`. To explore all available features and implementation details, check out the full class documentation. -All calculations are based -on [IBM Storage Insights](https://www.ibm.com/docs/en/storage-insights?topic=overview-units-measurement-storage-data). - ## Table of Contents * [Setup](#setup) @@ -24,6 +21,8 @@ on [IBM Storage Insights](https://www.ibm.com/docs/en/storage-insights?topic=ove * [More to Explore](#more-to-explore) ## Setup +The latest version of the library
+[![Latest Maven](https://badges.mvnrepository.com/badge/io.github.ardiien.datasize/datasize/badge.svg?label=Maven)](https://mvnrepository.com/artifact/io.github.ardiien.datasize/datasize) Kotlin DSL: @@ -51,18 +50,18 @@ dependencies { ## Basics -The `DataSize` has extensions available on numeric types like `Int`, `Long`, and `Double`. +The `DataSize` has extensions available on numeric types like `Int`, `Long`, and `Double`. Also, supports `ByteArray`. Here is a small example. ```kotlin import io.github.ardiien.datasize.* fun main() { - // Supported units: bytes, kilobytes, megabytes, gigabytes, and terabytes. + // Supported units: bytes, kilobytes, kibibytes, megabytes, mebibytes, etc. - val kilobyteFromInt = 1.kilobytes // 1024 - val kilobyteFromDouble = 1.0.kilobytes // 1024 - val kilobyteFromLong = 1L.kilobytes // 1024 + val kilobyteFromInt = 1.binary.kibibytes // 1024 + val kilobyteFromDouble = 1.0.decimal.kilobytes // 1024 + val kilobyteFromLong = 1L.binary.kibibytes // 1024 } ``` @@ -78,12 +77,12 @@ import io.github.ardiien.datasize.* fun main() { // Explore all overrides of the "operator fun" in DataSize class. - val addition = 5.megabytes + 15.megabytes // 20 MB - val substraction = 105.megabytes - 5.megabytes // 100 MB + val addition = 5.binary.mebibytes + 15.binary.mebibytes // 20 MB + val substraction = 105.decimal.megabytes - 5.decimal.megabytes // 100 MB - val multiplication = 5.megabytes * 2 // 10 MB - val division = 15.megabytes / 2 // 7,5 MB - val remainder = 11.megabytes % 2.megabytes // 1 MB + val multiplication = 5.decimal.megabytes * 2 // 10 MB + val division = 15.binary.mebibytes / 2 // 7,5 MB + val remainder = 11.decimal.megabytes % 2.decimal.megabytes // 1 MB } ``` @@ -107,16 +106,16 @@ import io.github.ardiien.datasize.* fun main() { val sortedList = listOf( - 1.kilobytes, 1.megabytes, 20.kilobytes // [1024, 20480, 1048576] + 1.binary.kibibytes, 1.binary.mebibytes, 20.binary.kibibytes // [1024, 20480, 1048576] ).sorted() - val gt = 15.kilobytes > 1.kilobytes // true - val lte = 15.kilobytes <= 14.kilobytes // false - val eq = 15.kilobytes == 15.kilobytes // true - val neq = 15.kilobytes != 5.kilobytes // true + val gt = 15.binary.kibibytes > 1.binary.kibibytes // true + val lte = 15.binary.kibibytes <= 14.binary.kibibytes // false + val eq = 15.binary.kibibytes == 15.binary.kibibytes // true + val neq = 15.binary.kibibytes != 5.binary.kibibytes // true - val min = min(2.megabytes, 2.kilobytes) // 2048 - val max = max(2.megabytes, 2.kilobytes) // 2097152 + val min = min(2.binary.mebibytes, 2.binary.kibibytes) // 2048 + val max = max(2.binary.mebibytes, 2.binary.kibibytes) // 2097152 } ``` @@ -129,12 +128,8 @@ bits (0s and 1s). You can have a certain number of bits, but you can't have a ne ## Formatting -The `DataSize` class supports formatting in both Decimal (`1000`) as _experimental_ and Binary (`1024`) base -representations. By default, `DataSizeUnit` uses the **Binary** base. -Currently, there is no way to select the base for the `DataSize`. To convert `DataSize` Binary to Decimal, use -`DataSize.toDecimalString` helper function exclusively for display purposes. - -To remove boilerplate and repeated code, you can use the utility class `DataSizeFormatter`. There are two types of +The `DataSize` class supports formatting in both Decimal (`1000`) and Binary (`1024`) base +representations. To remove boilerplate and repeated code, you can use the utility class `DataSizeFormatter`. There are two types of operations: 1. `format` – transforms a `DataSize` instance or raw bytes into a formatted `String` @@ -149,11 +144,12 @@ clamped between 0 and 2. Any value greater than 2 will be coerced to 2 during ev import io.github.ardiien.datasize.* fun main() { - val value = 55.563.kilobytes + val formatter = DefaultDataSizeFormatter(DefaultDataSizeFormatter.createFormat()) + val value = 55.563.binary.kibibytes - val defaultPrecision = DataSizeFormatter.format(value, decimals = 0) // 56 KB, default - val betterPrecision = DataSizeFormatter.format(value, decimals = 1) // 55,6 KB - val maxAvailablePrecision = DataSizeFormatter.format(value, decimals = 2) // 55,56 KB, max allowed + val defaultPrecision = formatter.format(value, fractionDigits = 0) // 56 KB, default + val betterPrecision = formatter.format(value, fractionDigits = 1) // 55,6 KB + val maxAvailablePrecision = formatter.format(value, fractionDigits = 2) // 55,56 KB, max allowed } ``` diff --git a/datasize-core/api/datasize-core.api b/datasize-core/api/datasize-core.api index bf900b2..b0ca492 100644 --- a/datasize-core/api/datasize-core.api +++ b/datasize-core/api/datasize-core.api @@ -1,100 +1,233 @@ public final class io/github/ardiien/datasize/DataSize : java/lang/Comparable { public static final field Companion Lio/github/ardiien/datasize/DataSize$Companion; - public static final synthetic fun box-impl (J)Lio/github/ardiien/datasize/DataSize; + public fun compareTo (Lio/github/ardiien/datasize/DataSize;)I public synthetic fun compareTo (Ljava/lang/Object;)I - public fun compareTo-BuKYR8w (J)I - public static fun compareTo-BuKYR8w (JJ)I - public static final fun div-BuKYR8w (JJ)D - public static final fun div-_mO8CwU (JD)J - public static final fun div-_mO8CwU (JI)J + public final fun div (D)Lio/github/ardiien/datasize/DataSize; + public final fun div (I)Lio/github/ardiien/datasize/DataSize; public fun equals (Ljava/lang/Object;)Z - public static fun equals-impl (JLjava/lang/Object;)Z - public static final fun equals-impl0 (JJ)Z - public static final fun getInBytes-impl (J)J - public static final fun getInGigabytes-impl (J)D - public static final fun getInKilobytes-impl (J)D - public static final fun getInMegabytes-impl (J)D - public static final fun getInTerabytes-impl (J)D + public final fun getInBytes ()J + public final fun getInGibibytes ()D + public final fun getInGigabytes ()D + public final fun getInKibibytes ()D + public final fun getInKilobytes ()D + public final fun getInMebibytes ()D + public final fun getInMegabytes ()D + public final fun getInPebibytes ()D + public final fun getInPetabytes ()D + public final fun getInTebibytes ()D + public final fun getInTerabytes ()D public fun hashCode ()I - public static fun hashCode-impl (J)I - public static final fun isInfinite-impl (J)Z - public static final fun isZero-impl (J)Z - public static final fun minus-nBzHbaQ (JJ)J - public static final fun plus-nBzHbaQ (JJ)J - public static final fun rem-nBzHbaQ (JJ)J - public static final fun times-_mO8CwU (JD)J - public static final fun times-_mO8CwU (JI)J - public static final fun toDecimalString-impl (JLio/github/ardiien/datasize/DataSizeUnit;I)Ljava/lang/String; - public static synthetic fun toDecimalString-impl$default (JLio/github/ardiien/datasize/DataSizeUnit;IILjava/lang/Object;)Ljava/lang/String; - public static final fun toDouble-impl (JLio/github/ardiien/datasize/DataSizeUnit;)D - public static final fun toInt-impl (JLio/github/ardiien/datasize/DataSizeUnit;)I - public static final fun toLong-impl (JLio/github/ardiien/datasize/DataSizeUnit;)J + public final fun isBinary ()Z + public final fun isDecimal ()Z + public final fun isInfinite ()Z + public final fun isZero ()Z + public final fun minus (Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/DataSize; + public final fun plus (Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/DataSize; + public final fun rem (Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/DataSize; + public final fun times (D)Lio/github/ardiien/datasize/DataSize; + public final fun times (I)Lio/github/ardiien/datasize/DataSize; + public final fun toDouble (Lio/github/ardiien/datasize/unit/DataSizeUnit;)D + public final fun toInt (Lio/github/ardiien/datasize/unit/DataSizeUnit;)I + public final fun toLong (Lio/github/ardiien/datasize/unit/DataSizeUnit;)J public fun toString ()Ljava/lang/String; - public static fun toString-impl (J)Ljava/lang/String; - public static final fun toString-impl (JLio/github/ardiien/datasize/DataSizeUnit;I)Ljava/lang/String; - public static synthetic fun toString-impl$default (JLio/github/ardiien/datasize/DataSizeUnit;IILjava/lang/Object;)Ljava/lang/String; - public final synthetic fun unbox-impl ()J + public final fun toString (Lio/github/ardiien/datasize/unit/DataSizeUnit;ILio/github/ardiien/datasize/DataSizeFormatter;)Ljava/lang/String; + public static synthetic fun toString$default (Lio/github/ardiien/datasize/DataSize;Lio/github/ardiien/datasize/unit/DataSizeUnit;ILio/github/ardiien/datasize/DataSizeFormatter;ILjava/lang/Object;)Ljava/lang/String; } public final class io/github/ardiien/datasize/DataSize$Companion { - public final fun convert (DLio/github/ardiien/datasize/DataSizeUnit;Lio/github/ardiien/datasize/DataSizeUnit;I)D - public static synthetic fun convert$default (Lio/github/ardiien/datasize/DataSize$Companion;DLio/github/ardiien/datasize/DataSizeUnit;Lio/github/ardiien/datasize/DataSizeUnit;IILjava/lang/Object;)D - public final fun getBytes-_mO8CwU (D)J - public final fun getBytes-_mO8CwU (I)J - public final fun getBytes-_mO8CwU (J)J - public final fun getGigabytes-_mO8CwU (D)J - public final fun getGigabytes-_mO8CwU (I)J - public final fun getGigabytes-_mO8CwU (J)J - public final fun getInfinite-3VZmxlE ()J - public final fun getKilobytes-_mO8CwU (D)J - public final fun getKilobytes-_mO8CwU (I)J - public final fun getKilobytes-_mO8CwU (J)J - public final fun getMegabytes-_mO8CwU (D)J - public final fun getMegabytes-_mO8CwU (I)J - public final fun getMegabytes-_mO8CwU (J)J - public final fun getTerabytes-_mO8CwU (D)J - public final fun getTerabytes-_mO8CwU (I)J - public final fun getTerabytes-_mO8CwU (J)J - public final fun getZero-3VZmxlE ()J -} - -public final class io/github/ardiien/datasize/DataSizeFormatter { - public static final field INSTANCE Lio/github/ardiien/datasize/DataSizeFormatter; - public static final fun format (JLio/github/ardiien/datasize/DataSizeUnit;I)Ljava/lang/String; - public static synthetic fun format$default (JLio/github/ardiien/datasize/DataSizeUnit;IILjava/lang/Object;)Ljava/lang/String; - public final synthetic fun format-wBTIb-E (JLio/github/ardiien/datasize/DataSizeUnit;I)Ljava/lang/String; - public static synthetic fun format-wBTIb-E$default (Lio/github/ardiien/datasize/DataSizeFormatter;JLio/github/ardiien/datasize/DataSizeUnit;IILjava/lang/Object;)Ljava/lang/String; - public static final fun unitFrom (J)Lio/github/ardiien/datasize/DataSizeUnit; - public final synthetic fun unitFrom-BuKYR8w (J)Lio/github/ardiien/datasize/DataSizeUnit; + public final fun getBinary (D)Lio/github/ardiien/datasize/builder/BinaryNumberDataSizeBuilder; + public final fun getBinary (I)Lio/github/ardiien/datasize/builder/BinaryNumberDataSizeBuilder; + public final fun getBinary (J)Lio/github/ardiien/datasize/builder/BinaryNumberDataSizeBuilder; + public final fun getBinary ([B)Lio/github/ardiien/datasize/builder/BinaryArrayDataSizeBuilder; + public final fun getDecimal (D)Lio/github/ardiien/datasize/builder/DecimalNumberDataSizeBuilder; + public final fun getDecimal (I)Lio/github/ardiien/datasize/builder/DecimalNumberDataSizeBuilder; + public final fun getDecimal (J)Lio/github/ardiien/datasize/builder/DecimalNumberDataSizeBuilder; + public final fun getDecimal ([B)Lio/github/ardiien/datasize/builder/DecimalArrayDataSizeBuilder; } -public final class io/github/ardiien/datasize/DataSizeKt { - public static final fun max-yHBNY8A (JJ)J - public static final fun min-yHBNY8A (JJ)J - public static final fun orZero-SwkpY8w (Lio/github/ardiien/datasize/DataSize;)J - public static final fun toDataSize (DLio/github/ardiien/datasize/DataSizeUnit;)J - public static final fun toDataSize (ILio/github/ardiien/datasize/DataSizeUnit;)J - public static final fun toDataSize (JLio/github/ardiien/datasize/DataSizeUnit;)J +public final class io/github/ardiien/datasize/DataSize$binary { + public static final field INSTANCE Lio/github/ardiien/datasize/DataSize$binary; + public final fun getInfinite ()Lio/github/ardiien/datasize/DataSize; + public final fun getZero ()Lio/github/ardiien/datasize/DataSize; +} + +public final class io/github/ardiien/datasize/DataSize$decimal { + public static final field INSTANCE Lio/github/ardiien/datasize/DataSize$decimal; + public final fun getInfinite ()Lio/github/ardiien/datasize/DataSize; + public final fun getZero ()Lio/github/ardiien/datasize/DataSize; } -public final class io/github/ardiien/datasize/DataSizeUnit : java/lang/Enum { - public static final field Bytes Lio/github/ardiien/datasize/DataSizeUnit; - public static final field Gigabytes Lio/github/ardiien/datasize/DataSizeUnit; - public static final field Kilobytes Lio/github/ardiien/datasize/DataSizeUnit; - public static final field Megabytes Lio/github/ardiien/datasize/DataSizeUnit; - public static final field Terabytes Lio/github/ardiien/datasize/DataSizeUnit; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lio/github/ardiien/datasize/DataSizeUnit; - public static fun values ()[Lio/github/ardiien/datasize/DataSizeUnit; +public abstract interface class io/github/ardiien/datasize/DataSizeFormatter { + public abstract fun format (Lio/github/ardiien/datasize/DataSize;Lio/github/ardiien/datasize/unit/DataSizeUnit;I)Ljava/lang/String; + public static synthetic fun format$default (Lio/github/ardiien/datasize/DataSizeFormatter;Lio/github/ardiien/datasize/DataSize;Lio/github/ardiien/datasize/unit/DataSizeUnit;IILjava/lang/Object;)Ljava/lang/String; + public abstract fun unitFrom (Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/unit/DataSizeUnit; } -public final class io/github/ardiien/datasize/DataSizeUnitKt { - public static final fun toBinaryUnit (DLio/github/ardiien/datasize/DataSizeUnit;)D - public static final fun toBinaryUnit (ILio/github/ardiien/datasize/DataSizeUnit;)D - public static final fun toDecimalUnit (DLio/github/ardiien/datasize/DataSizeUnit;)D - public static final fun toDecimalUnit (ILio/github/ardiien/datasize/DataSizeUnit;)D +public final class io/github/ardiien/datasize/DataSizeFormatter$DefaultImpls { + public static synthetic fun format$default (Lio/github/ardiien/datasize/DataSizeFormatter;Lio/github/ardiien/datasize/DataSize;Lio/github/ardiien/datasize/unit/DataSizeUnit;IILjava/lang/Object;)Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/DataSizeKt { + public static final fun max (Lio/github/ardiien/datasize/DataSize;Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/DataSize; + public static final fun min (Lio/github/ardiien/datasize/DataSize;Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/DataSize; + public static final fun orBinaryZero (Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/DataSize; + public static final fun orDecimalZero (Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/DataSize; } public abstract interface annotation class io/github/ardiien/datasize/ExperimentalDataSizeApi : java/lang/annotation/Annotation { } +public final class io/github/ardiien/datasize/builder/BinaryArrayDataSizeBuilder { + public fun (Ljava/lang/Number;)V + public final fun getBytes ()Lio/github/ardiien/datasize/DataSize; + public final fun toDataSize (Lio/github/ardiien/datasize/unit/BinaryUnit;)Lio/github/ardiien/datasize/DataSize; +} + +public final class io/github/ardiien/datasize/builder/BinaryNumberDataSizeBuilder { + public fun (Ljava/lang/Number;)V + public final fun getBytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getGibibytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getKibibytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getMebibytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getPebibytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getTebibytes ()Lio/github/ardiien/datasize/DataSize; + public final fun toDataSize (Lio/github/ardiien/datasize/unit/BinaryUnit;)Lio/github/ardiien/datasize/DataSize; +} + +public final class io/github/ardiien/datasize/builder/DecimalArrayDataSizeBuilder { + public fun (Ljava/lang/Number;)V + public final fun getBytes ()Lio/github/ardiien/datasize/DataSize; + public final fun toDataSize (Lio/github/ardiien/datasize/unit/DecimalUnit;)Lio/github/ardiien/datasize/DataSize; +} + +public final class io/github/ardiien/datasize/builder/DecimalNumberDataSizeBuilder { + public fun (Ljava/lang/Number;)V + public final fun getBytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getGigabytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getKilobytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getMegabytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getPetabytes ()Lio/github/ardiien/datasize/DataSize; + public final fun getTerabytes ()Lio/github/ardiien/datasize/DataSize; + public final fun toDataSize (Lio/github/ardiien/datasize/unit/DecimalUnit;)Lio/github/ardiien/datasize/DataSize; +} + +public final class io/github/ardiien/datasize/formatter/DefaultDataSizeFormatter : io/github/ardiien/datasize/DataSizeFormatter { + public static final field Companion Lio/github/ardiien/datasize/formatter/DefaultDataSizeFormatter$Companion; + public fun (Ljava/text/DecimalFormat;)V + public fun format (Lio/github/ardiien/datasize/DataSize;Lio/github/ardiien/datasize/unit/DataSizeUnit;I)Ljava/lang/String; + public fun unitFrom (Lio/github/ardiien/datasize/DataSize;)Lio/github/ardiien/datasize/unit/DataSizeUnit; +} + +public final class io/github/ardiien/datasize/formatter/DefaultDataSizeFormatter$Companion { + public final fun createFormat (Ljava/math/RoundingMode;IZLjava/text/DecimalFormatSymbols;)Ljava/text/DecimalFormat; + public static synthetic fun createFormat$default (Lio/github/ardiien/datasize/formatter/DefaultDataSizeFormatter$Companion;Ljava/math/RoundingMode;IZLjava/text/DecimalFormatSymbols;ILjava/lang/Object;)Ljava/text/DecimalFormat; + public final fun createFormatSymbols (CC)Ljava/text/DecimalFormatSymbols; + public static synthetic fun createFormatSymbols$default (Lio/github/ardiien/datasize/formatter/DefaultDataSizeFormatter$Companion;CCILjava/lang/Object;)Ljava/text/DecimalFormatSymbols; +} + +public abstract class io/github/ardiien/datasize/unit/BinaryUnit : io/github/ardiien/datasize/unit/DataSizeUnit { + public synthetic fun (Ljava/lang/String;Ljava/lang/String;DILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun entries ()Lkotlinx/collections/immutable/ImmutableList; +} + +public final class io/github/ardiien/datasize/unit/BinaryUnit$Byte : io/github/ardiien/datasize/unit/BinaryUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/BinaryUnit$Byte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/BinaryUnit$Gibibyte : io/github/ardiien/datasize/unit/BinaryUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/BinaryUnit$Gibibyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/BinaryUnit$Kibibyte : io/github/ardiien/datasize/unit/BinaryUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/BinaryUnit$Kibibyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/BinaryUnit$Mebibyte : io/github/ardiien/datasize/unit/BinaryUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/BinaryUnit$Mebibyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/BinaryUnit$Pebibyte : io/github/ardiien/datasize/unit/BinaryUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/BinaryUnit$Pebibyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/BinaryUnit$Tebibyte : io/github/ardiien/datasize/unit/BinaryUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/BinaryUnit$Tebibyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract class io/github/ardiien/datasize/unit/DataSizeUnit { + public final fun abbreviation ()Ljava/lang/String; + public fun entries ()Lkotlinx/collections/immutable/ImmutableList; + public final fun name ()Ljava/lang/String; + public final fun value ()Ljava/math/BigDecimal; +} + +public final class io/github/ardiien/datasize/unit/DataSizeUnitKt { + public static final fun isBinaryUnit (Lio/github/ardiien/datasize/unit/DataSizeUnit;)Z + public static final fun isDecimalUnit (Lio/github/ardiien/datasize/unit/DataSizeUnit;)Z +} + +public abstract class io/github/ardiien/datasize/unit/DecimalUnit : io/github/ardiien/datasize/unit/DataSizeUnit { + public synthetic fun (Ljava/lang/String;Ljava/lang/String;DILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun entries ()Lkotlinx/collections/immutable/ImmutableList; +} + +public final class io/github/ardiien/datasize/unit/DecimalUnit$Byte : io/github/ardiien/datasize/unit/DecimalUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/DecimalUnit$Byte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/DecimalUnit$Gigabyte : io/github/ardiien/datasize/unit/DecimalUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/DecimalUnit$Gigabyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/DecimalUnit$Kilobyte : io/github/ardiien/datasize/unit/DecimalUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/DecimalUnit$Kilobyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/DecimalUnit$Megabyte : io/github/ardiien/datasize/unit/DecimalUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/DecimalUnit$Megabyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/DecimalUnit$Petabyte : io/github/ardiien/datasize/unit/DecimalUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/DecimalUnit$Petabyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/github/ardiien/datasize/unit/DecimalUnit$Terabyte : io/github/ardiien/datasize/unit/DecimalUnit { + public static final field INSTANCE Lio/github/ardiien/datasize/unit/DecimalUnit$Terabyte; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/datasize-core/build.gradle.kts b/datasize-core/build.gradle.kts index fec62f6..8308f78 100644 --- a/datasize-core/build.gradle.kts +++ b/datasize-core/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "io.github.ardiien.datasize" -version = "1.0.0" +version = "2.0.0-alpha01" java { withSourcesJar() @@ -34,7 +34,7 @@ tasks.test { } dependencies { - implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.collections) testImplementation(libs.kotlin.test) } diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSize.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSize.kt index 2b81119..5f1a9ad 100644 --- a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSize.kt +++ b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSize.kt @@ -5,133 +5,129 @@ */ package io.github.ardiien.datasize -import java.math.RoundingMode -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import kotlin.math.floor -import kotlin.math.max -import kotlin.math.min -import kotlin.math.pow +import io.github.ardiien.datasize.DataSize.Companion.binary +import io.github.ardiien.datasize.DataSize.Companion.decimal +import io.github.ardiien.datasize.builder.BinaryArrayDataSizeBuilder +import io.github.ardiien.datasize.builder.BinaryNumberDataSizeBuilder +import io.github.ardiien.datasize.builder.DecimalArrayDataSizeBuilder +import io.github.ardiien.datasize.builder.DecimalNumberDataSizeBuilder +import io.github.ardiien.datasize.formatter.DefaultDataSizeFormatter +import io.github.ardiien.datasize.unit.BinaryUnit +import io.github.ardiien.datasize.unit.DataSizeUnit +import io.github.ardiien.datasize.unit.DecimalUnit +import io.github.ardiien.datasize.unit.DivisionMathContext +import io.github.ardiien.datasize.unit.UnitMathContext +import io.github.ardiien.datasize.unit.convertDataSizeUnit +import io.github.ardiien.datasize.unit.isBinaryUnit +import io.github.ardiien.datasize.unit.isDecimalUnit +import java.math.BigDecimal import kotlin.math.roundToInt -import kotlin.math.roundToLong /** - * Represents a data size in various units (e.g., bytes, kilobytes, megabytes, etc.). + * Represents a data size as a numeric value expressed in bytes with an associated [DataSizeUnit]. * - * This class provides methods for conversion between different data size units. - * To construct a [DataSize] use either the extension function [toDataSize], - * or the extension properties bytes, kilobytes, and megabytes, available on - * Int, Long, and Double numeric types. + * A [DataSize] is a value object whose magnitude is defined by its underlying byte value. + * All comparisons and arithmetic operations are performed using this canonical representation, + * regardless of the unit used to construct the instance. * - * To get the value of this [DataSize] expressed in a particular [DataSizeUnit]s use the functions - * toInt, toLong, and toDouble or the properties inBytes, inKilobytes, and inMegabytes. + * Instances are created via builder extensions: + * - [Long.binary], [Int.binary], [Double.binary], [ByteArray.binary]. + * - [Long.decimal], [Int.decimal], [Double.decimal], [ByteArray.decimal]. * - * For more information about unit calculations, see [Storage Insights](https://www.ibm.com/docs/en/storage-insights?topic=overview-units-measurement-storage-data). + * Conversion to other units is available through: + * - [toInt], [toLong], [toDouble]. + * - [inBytes], [inKilobytes], [inMegabytes], and related properties. */ -@JvmInline -public value class DataSize internal constructor( - private val rawValue: Long +public class DataSize internal constructor( + internal val rawValue: BigDecimal, + internal val unit: DataSizeUnit, ) : Comparable { - private val sizeUnit - get() = DataSizeUnit.Bytes init { - check(rawValue in 0..MAX_SIZE) { + check(rawValue in BigDecimal.ZERO..MAX_SIZE) { "DataSize must be in range 0 <= $rawValue <= $MAX_SIZE." } } - public companion object { - /** The size equal to exactly 0 bytes. */ - public val Zero: DataSize = DataSize(rawValue = 0) - - /** The size whose value is positive infinity. It is useful for representing unlimited size. */ - public val Infinite: DataSize = DataSize(rawValue = MAX_SIZE) - - - /** Returns a [DataSize] equal to this [Long] number of terabytes. */ - public inline val Long.terabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Terabytes) - - /** Returns a [DataSize] equal to this [Int] number of terabytes. */ - public inline val Int.terabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Terabytes) - - /** Returns a [DataSize] equal to this [Double] number of terabytes. */ - public inline val Double.terabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Terabytes) - - - /** Returns a [DataSize] equal to this [Long] number of gigabytes. */ - public inline val Long.gigabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Gigabytes) + /** + * Returns `true` if both values represent the same number of bytes. + * The unit is not considered for equality. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DataSize) return false - /** Returns a [DataSize] equal to this [Int] number of gigabytes. */ - public inline val Int.gigabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Gigabytes) + return this.rawValue.compareTo(other.rawValue) == 0 + } - /** Returns a [DataSize] equal to this [Double] number of gigabytes. */ - public inline val Double.gigabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Gigabytes) + /** + * Returns a hash code based on the underlying byte value. + * + * The unit is not included to preserve consistency with [equals]. + */ + override fun hashCode(): Int = rawValue.hashCode() + /** Predefined values for the binary (IEC) unit system. */ + @Suppress("ClassName") + public object binary { - /** Returns a [DataSize] equal to this [Long] number of megabytes. */ - public inline val Long.megabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Megabytes) + /** The size equal to exactly 0 bytes. */ + public val Zero: DataSize = DataSize(rawValue = BigDecimal.ZERO, unit = BinaryUnit.Byte) - /** Returns a [DataSize] equal to this [Int] number of megabytes. */ - public inline val Int.megabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Megabytes) + /** The size whose value is positive infinity. It is useful for representing unlimited size. */ + public val Infinite: DataSize = DataSize(rawValue = MAX_SIZE, unit = BinaryUnit.Byte) + } - /** Returns a [DataSize] equal to this [Double] number of megabytes. */ - public inline val Double.megabytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Megabytes) + /** Predefined values for the decimal (SI) unit system. */ + @Suppress("ClassName") + public object decimal { + /** The size equal to exactly 0 bytes. */ + public val Zero: DataSize = DataSize(rawValue = BigDecimal.ZERO, unit = DecimalUnit.Byte) - /** Returns a [DataSize] equal to this [Long] number of kilobytes. */ - public inline val Long.kilobytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Kilobytes) + /** The size whose value is positive infinity. It is useful for representing unlimited size. */ + public val Infinite: DataSize = DataSize(rawValue = MAX_SIZE, unit = DecimalUnit.Byte) + } - /** Returns a [DataSize] equal to this [Int] number of kilobytes. */ - public inline val Int.kilobytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Kilobytes) + public companion object { - /** Returns a [DataSize] equal to this [Double] number of kilobytes. */ - public inline val Double.kilobytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Kilobytes) + /** Returns a [BinaryNumberDataSizeBuilder] initialized with this value. */ + public inline val Long.binary: BinaryNumberDataSizeBuilder + get() = BinaryNumberDataSizeBuilder(this) + /** Returns a [BinaryNumberDataSizeBuilder] initialized with this value. */ + public inline val Int.binary: BinaryNumberDataSizeBuilder + get() = BinaryNumberDataSizeBuilder(this) - /** Returns a [DataSize] equal to this [Long] number of bytes. */ - public inline val Long.bytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Bytes) + /** Returns a [BinaryNumberDataSizeBuilder] initialized with this value. */ + public inline val Double.binary: BinaryNumberDataSizeBuilder + get() = BinaryNumberDataSizeBuilder(this) - /** Returns a [DataSize] equal to this [Int] number of bytes. */ - public inline val Int.bytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Bytes) + /** Returns a [BinaryArrayDataSizeBuilder] initialized with this value. */ + public inline val ByteArray.binary: BinaryArrayDataSizeBuilder + get() = BinaryArrayDataSizeBuilder(this.size) - /** Returns a [DataSize] equal to this [Double] number of bytes. */ - public inline val Double.bytes: DataSize - get() = toDataSize(unit = DataSizeUnit.Bytes) + /** Returns a [DecimalNumberDataSizeBuilder] initialized with this value. */ + public inline val Long.decimal: DecimalNumberDataSizeBuilder + get() = DecimalNumberDataSizeBuilder(this) + /** Returns a [DecimalNumberDataSizeBuilder] initialized with this value. */ + public inline val Int.decimal: DecimalNumberDataSizeBuilder + get() = DecimalNumberDataSizeBuilder(this) - /** Returns a number directly converted from [sourceUnit] to [targetUnit] type. */ - public fun convert( - value: Double, - sourceUnit: DataSizeUnit, - targetUnit: DataSizeUnit, - precision: Int = 2, - ): Double = convertDataSizeUnit(value, sourceUnit, targetUnit) - .roundTo(precision) + /** Returns a [DecimalNumberDataSizeBuilder] initialized with this value. */ + public inline val Double.decimal: DecimalNumberDataSizeBuilder + get() = DecimalNumberDataSizeBuilder(this) - private fun Double.roundTo(precision: Int): Double { - val factor = 10.0.pow(precision.toDouble()) - return floor(this * factor) / factor - } + /** Returns a [DecimalArrayDataSizeBuilder] initialized with this value. */ + public inline val ByteArray.decimal: DecimalArrayDataSizeBuilder + get() = DecimalArrayDataSizeBuilder(this.size) } + public operator fun rem(other: DataSize): DataSize = - dataSizeOf(this.rawValue.rem(other.rawValue)) + normalizedDataSizeOf(rawValue.remainder(other.rawValue, DivisionMathContext), unit) /** * Divides the [rawValue] by the given [scale] and returns the result as [DataSize]. @@ -142,8 +138,8 @@ public value class DataSize internal constructor( public operator fun div(scale: Int): DataSize { require(scale > 0) { "scale $scale must be a positive value to perform division" } - val result = rawValue / scale - return dataSizeOf(value = result) + val result = rawValue.divide(BigDecimal(scale, UnitMathContext), DivisionMathContext) + return normalizedDataSizeOf(result, unit) } /** @@ -161,32 +157,15 @@ public value class DataSize internal constructor( require(scale > 0) { "scale $scale must be a positive value to perform division" } - val unit = sizeUnit - val result = toDouble(unit) / scale - return result.toDataSize(unit = unit) - } - - /** - * Divides the [rawValue], converted to a [Double], by the given [DataSize], - * and returns the result as a [Double] in the larger [sizeUnit] of the two. - * - * @param other The divisor for the operation. Must be a positive data size unit. - * @throws IllegalArgumentException if [other] is equal to zero. - */ - public operator fun div(other: DataSize): Double { - require(other.rawValue > 0) { - "data size unit $other must be a positive value to perform division" - } - - val coarserUnit = maxOf(this.sizeUnit, other.sizeUnit) - return this.toDouble(coarserUnit) / other.toDouble(coarserUnit) + val result = rawValue.divide(BigDecimal(scale, UnitMathContext), DivisionMathContext) + return normalizedDataSizeOf(result, unit) } public operator fun times(scale: Int): DataSize { - if (scale == 0) return Zero + if (scale == 0) return if (isBinary()) binary.Zero else decimal.Zero - val result = rawValue * scale - return dataSizeOfNormalized(value = result) + val result = rawValue.multiply(BigDecimal(scale, UnitMathContext), UnitMathContext) + return normalizedDataSizeOf(result, unit) } public operator fun times(scale: Double): DataSize { @@ -195,201 +174,165 @@ public value class DataSize internal constructor( return times(intScale) } - val unit = sizeUnit - val result = toDouble(unit) * scale - return result.toDataSize(unit = unit) + val result = rawValue.multiply(BigDecimal(scale, UnitMathContext), UnitMathContext) + return normalizedDataSizeOf(result, unit) } public operator fun plus(other: DataSize): DataSize { val result = rawValue + other.rawValue - return dataSizeOfNormalized(value = result) + return normalizedDataSizeOf(result, unit) } public operator fun minus(other: DataSize): DataSize { val result = rawValue - other.rawValue - return dataSizeOfNormalized(value = result) + return normalizedDataSizeOf(result, unit) } - /** Returns true, if the data size value is infinite. */ - public fun isInfinite(): Boolean = rawValue == Infinite.rawValue + /** Returns `true` if this value represents the maximum supported size. */ + public fun isInfinite(): Boolean = + if (isBinary()) rawValue == binary.Infinite.rawValue else rawValue == decimal.Infinite.rawValue + + /** Returns `true` if this value is equal to zero bytes. */ + public fun isZero(): Boolean = + if (isBinary()) rawValue == binary.Zero.rawValue else rawValue == decimal.Zero.rawValue + + /** Returns `true` if this instance uses a decimal (SI) unit. */ + public fun isDecimal(): Boolean = unit.isDecimalUnit() - /** Returns true, if the data size value is zero. */ - public fun isZero(): Boolean = rawValue == Zero.rawValue + /** Returns `true` if this instance uses a binary (IEC) unit. */ + public fun isBinary(): Boolean = unit.isBinaryUnit() - override fun compareTo(other: DataSize): Int = - this.rawValue.compareTo(other.rawValue) + /** Compares this value with another [DataSize] based on byte magnitude. */ + override fun compareTo(other: DataSize): Int = this.rawValue.compareTo(other.rawValue) /** - * Returns the value of this data size expressed as a [Double] number of the specified [DataSizeUnit]. - * The operation may involve rounding when the result cannot be represented exactly with a [Double] number. + * Returns this value expressed as a [Double] in the specified [unit]. + * Precision may be lost for large values due to floating-point representation. */ public fun toDouble(unit: DataSizeUnit): Double = convertDataSizeUnit( - value = rawValue.coerceIn(0, MAX_SIZE).toDouble(), - sourceUnit = sizeUnit, + value = rawValue, + sourceUnit = this.unit, targetUnit = unit, - ) + ).stripTrailingZeros().toDouble() /** - * Returns the value of this data size expressed as a [Int] number of the specified [DataSizeUnit]. - * If the result doesn't fit in the range of [Int] type, it is coerced into that range. + * Returns this value expressed as an [Int] in the specified [unit]. + * Values outside the [Int] range are coerced. */ public fun toInt(unit: DataSizeUnit): Int = convertDataSizeUnit( - value = rawValue.coerceIn(0, MAX_SIZE), - sourceUnit = sizeUnit, + value = rawValue, + sourceUnit = this.unit, targetUnit = unit, - ).toInt() + ).stripTrailingZeros().toInt() - /** Returns the value of this data size expressed as a [Long] number of the specified [DataSizeUnit]. */ + /** Returns this value expressed as a [Long] in the specified [unit]. */ public fun toLong(unit: DataSizeUnit): Long = convertDataSizeUnit( - value = rawValue.coerceIn(0, MAX_SIZE), - sourceUnit = sizeUnit, + value = rawValue, + sourceUnit = this.unit, targetUnit = unit, - ) + ).stripTrailingZeros().toLong() - /** The value of this [DataSize] expressed as a [Double] number of terabytes. */ + /** Returns this value expressed in pebibytes. */ + public val inPebibytes: Double + get() = toDouble(BinaryUnit.Pebibyte) + + /** Returns this value expressed in petabytes. */ + public val inPetabytes: Double + get() = toDouble(DecimalUnit.Petabyte) + + /** Returns this value expressed in tebibytes. */ + public val inTebibytes: Double + get() = toDouble(BinaryUnit.Tebibyte) + + /** Returns this value expressed in terabytes. */ public val inTerabytes: Double - get() = toDouble(unit = DataSizeUnit.Terabytes) + get() = toDouble(DecimalUnit.Terabyte) - /** The value of this [DataSize] expressed as a [Double] number of gigabytes. */ + /** Returns this value expressed in gibibytes. */ + public val inGibibytes: Double + get() = toDouble(BinaryUnit.Gibibyte) + + /** Returns this value expressed in gigabytes. */ public val inGigabytes: Double - get() = toDouble(unit = DataSizeUnit.Gigabytes) + get() = toDouble(DecimalUnit.Gigabyte) + + /** Returns this value expressed in mebibytes. */ + public val inMebibytes: Double + get() = toDouble(BinaryUnit.Mebibyte) - /** The value of this [DataSize] expressed as a [Double] number of megabytes. */ + /** Returns this value expressed in megabytes. */ public val inMegabytes: Double - get() = toDouble(unit = DataSizeUnit.Megabytes) + get() = toDouble(DecimalUnit.Megabyte) - /** The value of this [DataSize] expressed as a [Double] number of kilobytes. */ + /** Returns this value expressed in kibibytes. */ + public val inKibibytes: Double + get() = toDouble(BinaryUnit.Kibibyte) + + /** Returns this value expressed in kilobytes. */ public val inKilobytes: Double - get() = toDouble(unit = DataSizeUnit.Kilobytes) + get() = toDouble(DecimalUnit.Kilobyte) - /** The value of this [DataSize] expressed as a [Long] number of bytes. */ + /** Returns this value expressed in bytes. */ public val inBytes: Long - get() = toLong(unit = DataSizeUnit.Bytes) + get() = toLong(if (isBinary()) BinaryUnit.Byte else DecimalUnit.Byte) /** - * Returns a string representation of this data size value expressed in the given [unit] - * and formatted with the specified [decimals] number of digits after decimal point. - * - * Special case: - * - an infinite data size is formatted as `"Infinity"` without a unit. + * Returns a string representation of this value in bytes. * - * @param decimals the number of digits after decimal point to show. The value must be non-negative. - * No more than 2 decimals will be shown, even if a larger number is requested. - * @return the value of data size in the specified [unit] followed by that unit abbreviated name: `B`, `KB`, `MB`, `GB`, or `TB`. - * @throws IllegalArgumentException if [decimals] is less than zero. + * - Returns `"Infinity"` for infinite values. + * - Otherwise returns the byte value without unit suffix. */ - public fun toString(unit: DataSizeUnit, decimals: Int = 0): String { - require(decimals >= 0) { "decimals must not be negative, but was $decimals" } - - val number = toDouble(unit) - if (number.isInfinite()) return number.toString() - - return "${createFormatForDecimals(number, decimals.coerceAtMost(2)).format(number)} ${unit.shortName()}" + override fun toString(): String = when { + isInfinite() -> "Infinity" + isBinary() -> toLong(BinaryUnit.Byte).toString() + isDecimal() -> toLong(DecimalUnit.Byte).toString() + else -> "0" } /** - * Returns a string representation of this data size value in decimal base expressed in the given [unit] - * and formatted with the specified [decimals] number of digits after decimal point. + * Returns a formatted string representation of this value. + * + * The value is converted to the specified [unit] and formatted using [formatter]. * * Special case: - * - an infinite data size is formatted as `"Infinity"` without a unit. + * - Infinite values are formatted as `"Infinity"` without a unit. * - * @param decimals the number of digits after decimal point to show. The value must be non-negative. - * No more than 2 decimals will be shown, even if a larger number is requested. - * @return the value of data size in the specified [unit] followed by that unit abbreviated name: `B`, `KB`, `MB`, `GB`, or `TB`. - * @throws IllegalArgumentException if [decimals] is less than zero. + * @param unit the unit to express the value in. + * @param fractionDigits number of digits after the decimal point (must be non-negative). + * @param formatter formatting strategy used to produce the output. + * + * @throws IllegalArgumentException if [fractionDigits] is negative. */ - @ExperimentalDataSizeApi - public fun toDecimalString(unit: DataSizeUnit, decimals: Int = 0): String { - require(decimals >= 0) { "decimals must be not negative, but was $decimals" } - - val number = toDecimalUnit(toDouble(unit), unit) - if (number.isInfinite()) return number.toString() - - return "${createFormatForDecimals(number, decimals.coerceAtMost(2)).format(number)} ${unit.shortName()}" - } - - private fun createFormatForDecimals(number: Double, decimals: Int) = - DecimalFormat("0").apply { - if (decimals > 0) maximumFractionDigits = decimals - roundingMode = RoundingMode.HALF_UP - decimalFormatSymbols = DecimalFormatSymbols().apply { - decimalSeparator = ',' - groupingSeparator = '.' - isGroupingUsed = number >= 10000 - groupingSize = 3 - } - } + public fun toString( + unit: DataSizeUnit, + fractionDigits: Int = 0, + formatter: DataSizeFormatter = DefaultDataSizeFormatter(DefaultDataSizeFormatter.createFormat()), + ): String = formatter.format(this, unit, fractionDigits) } -/** - * Returns [DataSize] converted from a double with [DataSizeUnit] type. - * - * @throws IllegalArgumentException value is Not-a-Number(NaN) or infinite. - * @throws IllegalStateException value is not within 0 <= value <= [MAX_SIZE] - */ -public fun Double.toDataSize(unit: DataSizeUnit): DataSize { - require(!this.isNaN() && !this.isInfinite()) { "DataSizeUnit value cannot be NaN or Infinite." } +// Maximum representable value (~9 exabytes). +internal val MAX_SIZE: BigDecimal = BigDecimal(Long.MAX_VALUE, UnitMathContext) - val value = convertDataSizeUnit( - value = this, - sourceUnit = unit, - targetUnit = DataSizeUnit.Bytes, - ).roundToLong() +private fun normalizedDataSizeOf(value: BigDecimal, unit: DataSizeUnit): DataSize = + DataSize(value.coerceIn(BigDecimal.ZERO, MAX_SIZE), unit) - return DataSize(value) -} -/** - * Returns [DataSize] converted from an integer with [DataSizeUnit] type. - * - * @throws IllegalStateException value is not within 0 <= value <= [MAX_SIZE] - */ -public fun Int.toDataSize(unit: DataSizeUnit): DataSize { - val value = convertDataSizeUnit( - value = this.toLong(), - sourceUnit = unit, - targetUnit = DataSizeUnit.Bytes, - ) - - return DataSize(value) -} +/** Returns the larger of two [DataSize] values (byte-based comparison). */ +public fun max(a: DataSize, b: DataSize): DataSize = if (a.rawValue >= b.rawValue) a else b -/** - * Returns [DataSize] converted from a long with [DataSizeUnit] type. - * - * @throws IllegalStateException value is not within 0 <= value <= [MAX_SIZE] - */ -public fun Long.toDataSize(unit: DataSizeUnit): DataSize { - val value = convertDataSizeUnit( - value = this, - sourceUnit = unit, - targetUnit = DataSizeUnit.Bytes, - ) - - return DataSize(value) -} - -public fun max(a: DataSize, b: DataSize): DataSize = - dataSizeOf(max(a.inBytes, b.inBytes)) +/** Returns the smaller of two [DataSize] values (byte-based comparison). */ +public fun min(a: DataSize, b: DataSize): DataSize = if (a.rawValue <= b.rawValue) a else b -public fun min(a: DataSize, b: DataSize): DataSize = - dataSizeOf(min(a.inBytes, b.inBytes)) - -// Max size of 8 exabytes should be enough. -internal const val MAX_SIZE = Long.MAX_VALUE - -private fun dataSizeOfNormalized(value: Long) = - dataSizeOf(value.coerceIn(0, MAX_SIZE)) - -private fun dataSizeOf(value: Long) = DataSize(value) +/** Returns value or binary zero if null.*/ +@Suppress("NOTHING_TO_INLINE") +public inline fun DataSize?.orBinaryZero(): DataSize = this ?: DataSize.binary.Zero -/** Returns the specified [DataSize] if not `null`, or [DataSize.Zero] otherwise. */ +/** Returns value or decimal zero if null.*/ @Suppress("NOTHING_TO_INLINE") -public inline fun DataSize?.orZero(): DataSize = this ?: DataSize.Zero +public inline fun DataSize?.orDecimalZero(): DataSize = this ?: DataSize.decimal.Zero diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSizeFormatter.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSizeFormatter.kt index fb24734..417edae 100644 --- a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSizeFormatter.kt +++ b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSizeFormatter.kt @@ -5,65 +5,36 @@ */ package io.github.ardiien.datasize -import io.github.ardiien.datasize.DataSize.Companion.bytes +import io.github.ardiien.datasize.unit.DataSizeUnit -/** A utility class for formatting data size values in a human-readable form. */ -public object DataSizeFormatter { +/** Provides utilities for formatting [DataSize] values into human-readable strings. */ +public interface DataSizeFormatter { /** - * Returns [DataSizeUnit] from [value] depending on the threshold. + * Returns the most appropriate [DataSizeUnit] for the given [value] based on its magnitude. + * The selected unit is the largest unit whose byte value does not exceed the given [value]. * - * @throws IllegalStateException value is not within 0 <= value <= [MAX_SIZE] + * @throws IllegalStateException if the value is not within the supported range */ - @JvmSynthetic - public fun unitFrom(value: DataSize): DataSizeUnit = - when { - value.inBytes > TERA_SCALE -> DataSizeUnit.Terabytes - value.inBytes > GIGA_SCALE -> DataSizeUnit.Gigabytes - value.inBytes > MEGA_SCALE -> DataSizeUnit.Megabytes - value.inBytes > KILO_SCALE -> DataSizeUnit.Kilobytes - else -> DataSizeUnit.Bytes - } + public fun unitFrom(value: DataSize): DataSizeUnit /** - * Returns a string representation of this data size [value] expressed in the given [unit] - * and formatted with the specified [decimals] number of digits after decimal point. + * Returns a string representation of the given [value] expressed in the specified [unit] + * and formatted with the given number of fractional [fractionDigits]. * - * @return String representation of [value] in a custom format. - * @throws IllegalStateException value is not within 0 <= value <= [MAX_SIZE] - * @throws IllegalArgumentException if [decimals] is less than zero. - * @see DataSize.toString - */ - @JvmSynthetic - public fun format( - value: DataSize, - unit: DataSizeUnit = unitFrom(value), - decimals: Int = 0, - ): String = value.toString(unit, decimals) - - /** - * Java-use only. - * For Kotlin, use unitFrom(value: DataSize) instead. + * If [unit] is not provided, a suitable unit is selected automatically using [unitFrom]. * - * @return [DataSizeUnit] from [value] depending on the threshold. - * @throws IllegalStateException value is not within 0 <= value <= [MAX_SIZE] - */ - @JvmStatic - @Throws(IllegalStateException::class) - public fun unitFrom(value: Long): DataSizeUnit = unitFrom(value.bytes) - - /** - * Java-use only. - * For Kotlin, use format(value: DataSize, unit: DataSizeUnit, decimals: Int) instead. + * @param value the data size to format. + * @param unit the unit to express the value in. + * @param fractionDigits the number of digits to display after the decimal point. * - * @return String representation of [value] in a custom format. + * @throws IllegalStateException if the value is not within the supported range. + * @throws IllegalArgumentException if [fractionDigits] is negative. */ - @JvmStatic - @Throws(IllegalStateException::class) public fun format( - value: Long, + value: DataSize, unit: DataSizeUnit = unitFrom(value), - decimals: Int = 0, - ): String = format(value.bytes, unit, decimals) + fractionDigits: Int = 0, + ): String } \ No newline at end of file diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSizeUnit.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSizeUnit.kt deleted file mode 100644 index 744ed33..0000000 --- a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/DataSizeUnit.kt +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2026 ardiien - * Licensed under the Apache License, Version 2.0. - * See http://www.apache.org/licenses/LICENSE-2.0 - */ -package io.github.ardiien.datasize - -import kotlin.math.floor -import kotlin.math.pow - - -internal const val BINARY_BASE: Long = 1024 -internal const val DECIMAL_BASE: Long = 1000 - -internal const val BYTE_SCALE: Long = 1 -internal const val KILO_SCALE: Long = BYTE_SCALE * BINARY_BASE -internal const val MEGA_SCALE: Long = KILO_SCALE * BINARY_BASE -internal const val GIGA_SCALE: Long = MEGA_SCALE * BINARY_BASE -internal const val TERA_SCALE: Long = GIGA_SCALE * BINARY_BASE - - -/** - * The list of possible size measurement units, in which a data size can be expressed. - * The smallest time unit is [Bytes] and the largest is [Terabytes]. - */ -public enum class DataSizeUnit(private val scale: Long) { - Terabytes(TERA_SCALE), - Gigabytes(GIGA_SCALE), - Megabytes(MEGA_SCALE), - Kilobytes(KILO_SCALE), - Bytes(BYTE_SCALE); - - - /** Converts the given size [value] of the current [scale] into the specified [unit]. */ - internal fun convert(value: Long, unit: DataSizeUnit): Long = - cvt(value, this.scale, unit.scale) - - internal companion object { - private fun cvt(v: Long, src: Long, dst: Long): Long { - return if (src == dst) v - else floor((v / (dst / src.toDouble()))).toLong() - } - } -} - -@ExperimentalDataSizeApi -public fun toDecimalUnit( - value: Double, - unit: DataSizeUnit, -): Double { - val power = unit.base() - val binaryBase = BINARY_BASE.toDouble().pow(power) - val decimalBase = DECIMAL_BASE.toDouble().pow(power) - - return value * (binaryBase / decimalBase) -} - -@ExperimentalDataSizeApi -public fun toDecimalUnit( - value: Int, - unit: DataSizeUnit, -): Double = toDecimalUnit(value.toDouble(), unit) - -public fun toBinaryUnit( - value: Double, - unit: DataSizeUnit, -): Double { - val power = unit.base() - val binaryBase = BINARY_BASE.toDouble().pow(power) - val decimalBase = DECIMAL_BASE.toDouble().pow(power) - - return value * (decimalBase / binaryBase) -} - -public fun toBinaryUnit( - value: Int, - unit: DataSizeUnit, -): Double = toBinaryUnit(value.toDouble(), unit) - -/** Converts the given size [value] expressed in the specified [sourceUnit] into the specified [targetUnit] ar [Long]. */ -internal fun convertDataSizeUnit( - value: Long, - sourceUnit: DataSizeUnit, - targetUnit: DataSizeUnit, -): Long = sourceUnit.convert(value, targetUnit) - -/** Converts the given size [value] expressed in the specified [sourceUnit] into the specified [targetUnit] ar [Double]. */ -internal fun convertDataSizeUnit( - value: Double, - sourceUnit: DataSizeUnit, - targetUnit: DataSizeUnit, -): Double { - if (sourceUnit == targetUnit) return value - - val sourceInTargets = targetUnit.convert(1, sourceUnit) - if (sourceInTargets > 0) - return (value / sourceInTargets) - - val otherInThis = sourceUnit.convert(1, targetUnit) - return (value * otherInThis) -} - -internal fun DataSizeUnit.shortName(): String = - when (this) { - DataSizeUnit.Terabytes -> "TB" - DataSizeUnit.Gigabytes -> "GB" - DataSizeUnit.Megabytes -> "MB" - DataSizeUnit.Kilobytes -> "KB" - DataSizeUnit.Bytes -> "B" - } - -internal fun DataSizeUnit.base(): Int = - when (this) { - DataSizeUnit.Terabytes -> 4 - DataSizeUnit.Gigabytes -> 3 - DataSizeUnit.Megabytes -> 2 - DataSizeUnit.Kilobytes -> 1 - DataSizeUnit.Bytes -> 0 - } \ No newline at end of file diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/BinaryArrayDataSizeBuilder.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/BinaryArrayDataSizeBuilder.kt new file mode 100644 index 0000000..4177671 --- /dev/null +++ b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/BinaryArrayDataSizeBuilder.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2026 ardiien + * Licensed under the Apache License, Version 2.0. + * See http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.ardiien.datasize.builder + +import io.github.ardiien.datasize.DataSize +import io.github.ardiien.datasize.ExperimentalDataSizeApi +import io.github.ardiien.datasize.unit.BinaryUnit +import io.github.ardiien.datasize.unit.convertDataSizeUnit + + +@ExperimentalDataSizeApi +public class BinaryArrayDataSizeBuilder(private val number: Number) { + + init { + check(number is Int) { + "DataSize must be constructed only with Int." + } + } + + /** Returns a [DataSize] representing this value in bytes. */ + public inline val bytes: DataSize + get() = toDataSize(unit = BinaryUnit.Byte) + + /** + * Converts this numeric value expressed in the given binary [unit] into a [DataSize]. + * The resulting [DataSize] is normalized to bytes. + * + * @throws IllegalStateException if the resulting value is not within the supported range. + */ + public fun toDataSize(unit: BinaryUnit): DataSize { + val targetUnit = BinaryUnit.Byte + val value = convertDataSizeUnit( + value = number, + sourceUnit = unit, + targetUnit = targetUnit, + ) + + return DataSize(value, targetUnit) + } +} \ No newline at end of file diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/BinaryNumberDataSizeBuilder.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/BinaryNumberDataSizeBuilder.kt new file mode 100644 index 0000000..b4dc57e --- /dev/null +++ b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/BinaryNumberDataSizeBuilder.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2026 ardiien + * Licensed under the Apache License, Version 2.0. + * See http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.ardiien.datasize.builder + +import io.github.ardiien.datasize.DataSize +import io.github.ardiien.datasize.ExperimentalDataSizeApi +import io.github.ardiien.datasize.unit.BinaryUnit +import io.github.ardiien.datasize.unit.convertDataSizeUnit + + +@ExperimentalDataSizeApi +public class BinaryNumberDataSizeBuilder(private val number: Number) { + + init { + check(number is Int || number is Long || number is Double) { + "DataSize must be constructed only with Int, Long, or Double." + } + } + + /** Returns a [DataSize] representing this value in pebibytes (PiB). */ + public inline val pebibytes: DataSize + get() = toDataSize(unit = BinaryUnit.Pebibyte) + + /** Returns a [DataSize] representing this value in tebibytes (TiB). */ + public inline val tebibytes: DataSize + get() = toDataSize(unit = BinaryUnit.Tebibyte) + + /** Returns a [DataSize] representing this value in gibibytes (GiB). */ + public inline val gibibytes: DataSize + get() = toDataSize(unit = BinaryUnit.Gibibyte) + + /** Returns a [DataSize] representing this value in mebibytes (MiB). */ + public inline val mebibytes: DataSize + get() = toDataSize(unit = BinaryUnit.Mebibyte) + + /** Returns a [DataSize] representing this value in kibibytes (KiB). */ + public inline val kibibytes: DataSize + get() = toDataSize(unit = BinaryUnit.Kibibyte) + + /** Returns a [DataSize] representing this value in bytes. */ + public inline val bytes: DataSize + get() = toDataSize(unit = BinaryUnit.Byte) + + /** + * Converts this numeric value expressed in the given binary [unit] into a [DataSize]. + * The resulting [DataSize] is normalized to bytes. + * + * @throws IllegalStateException if the resulting value is not within the supported range. + */ + public fun toDataSize(unit: BinaryUnit): DataSize { + val targetUnit = BinaryUnit.Byte + val value = convertDataSizeUnit( + value = number, + sourceUnit = unit, + targetUnit = targetUnit, + ) + + return DataSize(value, targetUnit) + } +} \ No newline at end of file diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/DecimalArrayDataSizeBuilder.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/DecimalArrayDataSizeBuilder.kt new file mode 100644 index 0000000..8fd712c --- /dev/null +++ b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/DecimalArrayDataSizeBuilder.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2026 ardiien + * Licensed under the Apache License, Version 2.0. + * See http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.ardiien.datasize.builder + +import io.github.ardiien.datasize.DataSize +import io.github.ardiien.datasize.ExperimentalDataSizeApi +import io.github.ardiien.datasize.unit.DecimalUnit +import io.github.ardiien.datasize.unit.convertDataSizeUnit + + +@ExperimentalDataSizeApi +public class DecimalArrayDataSizeBuilder(private val number: Number) { + + init { + check(number is Int) { + "DataSize must be constructed only with Int." + } + } + + /** Returns a [DataSize] representing this value in bytes. */ + public inline val bytes: DataSize + get() = toDataSize(unit = DecimalUnit.Byte) + + /** + * Converts this numeric value expressed in the given decimal [unit] into a [DataSize]. + * The resulting [DataSize] is normalized to bytes. + * + * @throws IllegalStateException if the resulting value is not within the supported range. + */ + public fun toDataSize(unit: DecimalUnit): DataSize { + val targetUnit = DecimalUnit.Byte + val value = convertDataSizeUnit( + value = number, + sourceUnit = unit, + targetUnit = targetUnit, + ) + + return DataSize(value, targetUnit) + } +} \ No newline at end of file diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/DecimalNumberDataSizeBuilder.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/DecimalNumberDataSizeBuilder.kt new file mode 100644 index 0000000..9d533d5 --- /dev/null +++ b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/builder/DecimalNumberDataSizeBuilder.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2026 ardiien + * Licensed under the Apache License, Version 2.0. + * See http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.ardiien.datasize.builder + +import io.github.ardiien.datasize.DataSize +import io.github.ardiien.datasize.ExperimentalDataSizeApi +import io.github.ardiien.datasize.unit.DecimalUnit +import io.github.ardiien.datasize.unit.convertDataSizeUnit + + +@ExperimentalDataSizeApi +public class DecimalNumberDataSizeBuilder(private val number: Number) { + + init { + check(number is Int || number is Long || number is Double) { + "DataSize must be constructed only with Int, Long, or Double." + } + } + + /** Returns a [DataSize] representing this value in petabytes (PB). */ + public inline val petabytes: DataSize + get() = toDataSize(unit = DecimalUnit.Petabyte) + + /** Returns a [DataSize] representing this value in terabytes (TB). */ + public inline val terabytes: DataSize + get() = toDataSize(unit = DecimalUnit.Terabyte) + + /** Returns a [DataSize] representing this value in gigabytes (GB). */ + public inline val gigabytes: DataSize + get() = toDataSize(unit = DecimalUnit.Gigabyte) + + /** Returns a [DataSize] representing this value in megabytes (MB). */ + public inline val megabytes: DataSize + get() = toDataSize(unit = DecimalUnit.Megabyte) + + /** Returns a [DataSize] representing this value in kilobytes (KB). */ + public inline val kilobytes: DataSize + get() = toDataSize(unit = DecimalUnit.Kilobyte) + + /** Returns a [DataSize] representing this value in bytes. */ + public inline val bytes: DataSize + get() = toDataSize(unit = DecimalUnit.Byte) + + /** + * Converts this numeric value expressed in the given decimal [unit] into a [DataSize]. + * The resulting [DataSize] is normalized to bytes. + * + * @throws IllegalStateException if the resulting value is not within the supported range. + */ + public fun toDataSize(unit: DecimalUnit): DataSize { + val targetUnit = DecimalUnit.Byte + val value = convertDataSizeUnit( + value = number, + sourceUnit = unit, + targetUnit = targetUnit, + ) + + return DataSize(value, targetUnit) + } +} \ No newline at end of file diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/formatter/DefaultDataSizeFormatter.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/formatter/DefaultDataSizeFormatter.kt new file mode 100644 index 0000000..1c1448e --- /dev/null +++ b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/formatter/DefaultDataSizeFormatter.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2026 ardiien + * Licensed under the Apache License, Version 2.0. + * See http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.ardiien.datasize.formatter + +import io.github.ardiien.datasize.DataSize +import io.github.ardiien.datasize.DataSizeFormatter +import io.github.ardiien.datasize.unit.DataSizeUnit +import java.math.RoundingMode +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols + + +public class DefaultDataSizeFormatter(private val formatter: DecimalFormat) : DataSizeFormatter { + + /** + * Returns the most appropriate [DataSizeUnit] for the given [value] based on its magnitude. + * The selected unit is the largest unit whose byte value does not exceed the given [value]. + * + * @throws IllegalStateException if the value is not within the supported range + */ + public override fun unitFrom(value: DataSize): DataSizeUnit { + val units = value.unit.entries() + return units.firstOrNull { value.rawValue > it.value() } ?: units.last() + } + + /** + * Returns a string representation of the given [value] expressed in the specified [unit] + * and formatted with the given number of fractional [fractionDigits]. + * + * If [unit] is not provided, a suitable unit is selected automatically using [unitFrom]. + * + * @param value the data size to format. + * @param unit the unit to express the value in. + * @param fractionDigits the number of digits to display after the decimal point. + * + * @throws IllegalStateException if the value is not within the supported range. + * @throws IllegalArgumentException if [fractionDigits] is negative. + */ + public override fun format( + value: DataSize, + unit: DataSizeUnit, + fractionDigits: Int, + ): String { + require(fractionDigits >= 0) { "fractionDigits must not be negative, but was $fractionDigits" } + + val number = value.toDouble(unit) + if (number.isInfinite()) return number.toString() + + val actualFormatter = extendFormat(formatter, number, fractionDigits) + return "${actualFormatter.format(number)} ${unit.abbreviation()}" + } + + public companion object { + + public fun createFormat( + roundingMode: RoundingMode = RoundingMode.HALF_UP, + groupingSize: Int = 3, + isGroupingUsed: Boolean = true, + decimalFormatSymbols: DecimalFormatSymbols = createFormatSymbols(), + ): DecimalFormat = DecimalFormat("0") + .apply { + this.roundingMode = roundingMode + this.isGroupingUsed = isGroupingUsed + this.groupingSize = groupingSize + this.decimalFormatSymbols = decimalFormatSymbols + } + + internal fun extendFormat(formatter: DecimalFormat, number: Double, fractionDigits: Int): DecimalFormat = + formatter.apply { + maximumFractionDigits = fractionDigits.coerceAtMost(2) + isGroupingUsed = number > 9999.999999999 + } + + public fun createFormatSymbols( + decimalSeparator: Char = ',', + groupingSeparator: Char = ' ', + ): DecimalFormatSymbols = DecimalFormatSymbols() + .apply { + this.decimalSeparator = decimalSeparator + this.groupingSeparator = groupingSeparator + } + } +} \ No newline at end of file diff --git a/datasize-core/src/main/kotlin/io/github/ardiien/datasize/unit/DataSizeUnit.kt b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/unit/DataSizeUnit.kt new file mode 100644 index 0000000..6f2c3a1 --- /dev/null +++ b/datasize-core/src/main/kotlin/io/github/ardiien/datasize/unit/DataSizeUnit.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2026 ardiien + * Licensed under the Apache License, Version 2.0. + * See http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.ardiien.datasize.unit + +import io.github.ardiien.datasize.ExperimentalDataSizeApi +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal +import java.math.MathContext +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + + +/** Base used for decimal units (powers of 10). */ +private const val SI_BASE: Double = 10.0 + +/** Base used for binary units (powers of 2). */ +private const val IEC_BASE: Double = 2.0 + + +/** + * Represents a unit of data size. + * + * Each unit defines a fixed number of bytes and belongs to a specific unit system + * (decimal or binary). Instances of this class are immutable and provide a + * type-safe way to work with data size units. + * + * @see Binary prefix + * @see File size + */ +@ExperimentalDataSizeApi +public abstract class DataSizeUnit internal constructor( + private val name: String, + private val abbreviation: String, + base: Double, + exponent: Int, +) { + private val value: BigDecimal = base.toBigDecimal().pow(exponent) + + /** Returns the full human-readable name of this unit. */ + public fun name(): String = name + + /** Returns the abbreviated symbol of this unit. */ + public fun abbreviation(): String = abbreviation + + /** Returns the exact number of bytes represented by a single unit. */ + public fun value(): BigDecimal = value + + /** Returns all units belonging to the same unit system, ordered from largest to smallest. */ + public open fun entries(): ImmutableList = persistentListOf() +} + +/** Returns `true` if this unit belongs to the decimal (SI) system. */ +@ExperimentalDataSizeApi +@OptIn(ExperimentalContracts::class) +public fun DataSizeUnit.isDecimalUnit(): Boolean { + contract { returns(true) implies (this@isDecimalUnit is DecimalUnit) } + return this is DecimalUnit +} + +/** Returns `true` if this unit belongs to the binary (IEC) system. */ +@ExperimentalDataSizeApi +@OptIn(ExperimentalContracts::class) +public fun DataSizeUnit.isBinaryUnit(): Boolean { + contract { returns(true) implies (this@isBinaryUnit is BinaryUnit) } + return this is BinaryUnit +} + +/** + * Represents decimal (SI) data size units based on powers of 10. + * Units range from [Byte] (10⁰) to [Petabyte] (10¹⁵). + */ +@ExperimentalDataSizeApi +public sealed class DecimalUnit( + name: String, + abbreviation: String, + base: Double, + exponent: Int, +) : DataSizeUnit(name, abbreviation, base, exponent) { + + public data object Byte : DecimalUnit(name = "bytes", abbreviation = "B", base = SI_BASE, exponent = 0) + public data object Kilobyte : DecimalUnit(name = "kilobytes", abbreviation = "KB", base = SI_BASE, exponent = 3) + public data object Megabyte : DecimalUnit(name = "megabytes", abbreviation = "MB", base = SI_BASE, exponent = 6) + public data object Gigabyte : DecimalUnit(name = "gigabytes", abbreviation = "GB", base = SI_BASE, exponent = 9) + public data object Terabyte : DecimalUnit(name = "terabytes", abbreviation = "TB", base = SI_BASE, exponent = 12) + public data object Petabyte : DecimalUnit(name = "petabytes", abbreviation = "PB", base = SI_BASE, exponent = 15) + + public override fun entries(): ImmutableList = decimalEntries +} + +private val decimalEntries: ImmutableList = persistentListOf( + DecimalUnit.Petabyte, + DecimalUnit.Terabyte, + DecimalUnit.Gigabyte, + DecimalUnit.Megabyte, + DecimalUnit.Kilobyte, + DecimalUnit.Byte, +) + +/** + * Represents binary (IEC) data size units based on powers of 2. + * Units range from [Byte] (2⁰) to [Pebibyte] (2⁵⁰). + * + * Note: For consistency with common user expectations, unit names and abbreviations + * use decimal-style labels (e.g., "KB", "MB"), even though the underlying values follow the binary system. + */ +@ExperimentalDataSizeApi +public sealed class BinaryUnit( + name: String, + abbreviation: String, + base: Double, + exponent: Int, +) : DataSizeUnit(name, abbreviation, base, exponent) { + + public data object Byte : BinaryUnit(name = "bytes", abbreviation = "B", base = IEC_BASE, exponent = 0) + public data object Kibibyte : BinaryUnit(name = "kilobytes", abbreviation = "KB", base = IEC_BASE, exponent = 10) + public data object Mebibyte : BinaryUnit(name = "megabytes", abbreviation = "MB", base = IEC_BASE, exponent = 20) + public data object Gibibyte : BinaryUnit(name = "gigabytes", abbreviation = "GB", base = IEC_BASE, exponent = 30) + public data object Tebibyte : BinaryUnit(name = "terabytes", abbreviation = "TB", base = IEC_BASE, exponent = 40) + public data object Pebibyte : BinaryUnit(name = "petabytes", abbreviation = "PB", base = IEC_BASE, exponent = 50) + + public override fun entries(): ImmutableList = binaryEntries +} + +private val binaryEntries: ImmutableList = persistentListOf( + BinaryUnit.Pebibyte, + BinaryUnit.Tebibyte, + BinaryUnit.Gibibyte, + BinaryUnit.Mebibyte, + BinaryUnit.Kibibyte, + BinaryUnit.Byte, +) + +internal fun convertDataSizeUnit( + value: Number, + sourceUnit: DataSizeUnit, + targetUnit: DataSizeUnit, +): BigDecimal { + val value = when (value) { + is Int -> BigDecimal(value, UnitMathContext) + is Long -> BigDecimal(value, UnitMathContext) + is Double -> { + require(!(value.isNaN() || value.isInfinite())) { "DataSizeUnit value cannot be NaN or Infinite." } + BigDecimal(value, UnitMathContext) + } + else -> error("Unsupported number type ${value::class}") + } + + if (sourceUnit == targetUnit) return value + + return sourceUnit.value() + .divide(targetUnit.value(), DivisionMathContext) + .multiply(value, UnitMathContext) +} + +internal fun convertDataSizeUnit( + value: BigDecimal, + sourceUnit: DataSizeUnit, + targetUnit: DataSizeUnit, +): BigDecimal { + if (sourceUnit == targetUnit) return value + + return sourceUnit.value() + .divide(targetUnit.value(), DivisionMathContext) + .multiply(value, UnitMathContext) +} + +internal val UnitMathContext: MathContext = MathContext.UNLIMITED +internal val DivisionMathContext: MathContext = MathContext.DECIMAL128 diff --git a/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DataSizeFormatterTest.kt b/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DataSizeFormatterTest.kt deleted file mode 100644 index f09af41..0000000 --- a/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DataSizeFormatterTest.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2026 ardiien - * Licensed under the Apache License, Version 2.0. - * See http://www.apache.org/licenses/LICENSE-2.0 - */ - -package io.github.ardiien.datasize - -import io.github.ardiien.datasize.DataSize.Companion.bytes -import io.github.ardiien.datasize.DataSize.Companion.gigabytes -import io.github.ardiien.datasize.DataSize.Companion.kilobytes -import io.github.ardiien.datasize.DataSize.Companion.megabytes -import io.github.ardiien.datasize.DataSize.Companion.terabytes -import kotlin.test.Test -import kotlin.test.assertEquals - - -class DataSizeFormatterTest { - - @Test - fun `Bytes is returned as data size unit when expected`() { - val expected = DataSizeUnit.Bytes - val size = 500.bytes.inBytes - val actualFromDataSize = DataSizeFormatter.unitFrom(size.bytes) - val actualFromRawValue = DataSizeFormatter.unitFrom(size) - assertEquals(expected, actualFromDataSize) - assertEquals(expected, actualFromRawValue) - } - - @Test - fun `Kilobytes is returned as data size unit when expected`() { - val expected = DataSizeUnit.Kilobytes - val size = 500.kilobytes.inBytes - val actualFromDataSize = DataSizeFormatter.unitFrom(size.bytes) - val actualFromRawValue = DataSizeFormatter.unitFrom(size) - assertEquals(expected, actualFromDataSize) - assertEquals(expected, actualFromRawValue) - } - - @Test - fun `Megabytes is returned as data size unit when expected`() { - val expected = DataSizeUnit.Megabytes - val size = 500.megabytes.inBytes - val actualFromDataSize = DataSizeFormatter.unitFrom(size.bytes) - val actualFromRawValue = DataSizeFormatter.unitFrom(size) - assertEquals(expected, actualFromDataSize) - assertEquals(expected, actualFromRawValue) - } - - @Test - fun `Gigabytes is returned as data size unit when expected`() { - val expected = DataSizeUnit.Gigabytes - val size = 500.gigabytes.inBytes - val actualFromDataSize = DataSizeFormatter.unitFrom(size.bytes) - val actualFromRawValue = DataSizeFormatter.unitFrom(size) - assertEquals(expected, actualFromDataSize) - assertEquals(expected, actualFromRawValue) - } - - @Test - fun `Terabytes is returned as data size unit when expected`() { - val expected = DataSizeUnit.Terabytes - val size = 500.terabytes.inBytes - val actualFromDataSize = DataSizeFormatter.unitFrom(size.bytes) - val actualFromRawValue = DataSizeFormatter.unitFrom(size) - assertEquals(expected, actualFromDataSize) - assertEquals(expected, actualFromRawValue) - } - - @Test - fun `Format method returns correct string with 0 decimals`() { - val expected = "5 GB" - val size = 5.32.gigabytes.inBytes - val actualFromDataSize = - DataSizeFormatter.format( - value = size.bytes, - unit = DataSizeUnit.Gigabytes, - decimals = 0 - ) - val actualFromRawValue = DataSizeFormatter.format( - value = size, - unit = DataSizeUnit.Gigabytes, - decimals = 0 - ) - assertEquals(expected, actualFromDataSize) - assertEquals(expected, actualFromRawValue) - } - - @Test - fun `Format method returns correct string with 1 decimals`() { - val expected = "5,3 GB" - val size = 5.32.gigabytes.inBytes - val actualFromDataSize = - DataSizeFormatter.format( - value = size.bytes, - unit = DataSizeUnit.Gigabytes, - decimals = 1 - ) - val actualFromRawValue = DataSizeFormatter.format( - value = size, - unit = DataSizeUnit.Gigabytes, - decimals = 1 - ) - assertEquals(expected, actualFromDataSize) - assertEquals(expected, actualFromRawValue) - } - - @Test - fun `Format method returns correct string with 2 decimals`() { - val expected = "5,32 GB" - val size = 5.32.gigabytes.inBytes - val actualFromDataSize = - DataSizeFormatter.format( - value = size.bytes, - unit = DataSizeUnit.Gigabytes, - decimals = 2 - ) - val actualFromRawValue = DataSizeFormatter.format( - value = size, - unit = DataSizeUnit.Gigabytes, - decimals = 2 - ) - assertEquals(expected, actualFromDataSize) - assertEquals(expected, actualFromRawValue) - } -} \ No newline at end of file diff --git a/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DataSizeTest.kt b/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DataSizeTest.kt index 5520617..bbecf14 100644 --- a/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DataSizeTest.kt +++ b/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DataSizeTest.kt @@ -3,408 +3,348 @@ * Licensed under the Apache License, Version 2.0. * See http://www.apache.org/licenses/LICENSE-2.0 */ - package io.github.ardiien.datasize +import io.github.ardiien.datasize.DataSize.Companion.binary +import io.github.ardiien.datasize.DataSize.Companion.decimal +import io.github.ardiien.datasize.unit.BinaryUnit +import io.github.ardiien.datasize.unit.DecimalUnit +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows -import io.github.ardiien.datasize.DataSize.Companion.bytes -import io.github.ardiien.datasize.DataSize.Companion.gigabytes -import io.github.ardiien.datasize.DataSize.Companion.kilobytes -import io.github.ardiien.datasize.DataSize.Companion.megabytes -import io.github.ardiien.datasize.DataSize.Companion.terabytes import kotlin.test.Test -import kotlin.test.assertEquals class DataSizeTest { @Test - fun `negative number in DataSizeUnit init throws IllegalStateException`() { + fun `negative value throws on DataSizeUnit init`() { + assertThrows(IllegalStateException::class.java) { + (-3).decimal.kilobytes + } assertThrows(IllegalStateException::class.java) { - (-3).kilobytes + (-3).binary.kibibytes } } @Test - fun `negative number in toDataSize init throws IllegalStateException`() { + fun `negative value throws on toDataSize`() { + assertThrows(IllegalStateException::class.java) { + (-3).decimal.toDataSize(DecimalUnit.Kilobyte) + } assertThrows(IllegalStateException::class.java) { - (-3).toDataSize(DataSizeUnit.Kilobytes) + (-3).binary.toDataSize(BinaryUnit.Kibibyte) } } @Test - fun `zero byte object in number expression returns zero`() { - val subject = DataSize.Zero + fun `zero DataSize returns zero for all units`() { + val subjectBinary = DataSize.binary.Zero + val subjectDecimal = DataSize.decimal.Zero - val bytes = subject.inBytes - val kilobytes = subject.inKilobytes - val megabytes = subject.inMegabytes - val gigabytes = subject.inGigabytes - val terabytes = subject.inTerabytes + val binaryBytes = subjectBinary.inBytes + val decimalBytes = subjectDecimal.inBytes + val kilobytes = subjectBinary.inKilobytes + val kibibytes = subjectBinary.inKibibytes + val megabytes = subjectBinary.inMegabytes + val mebibytes = subjectBinary.inMebibytes + val gigabytes = subjectBinary.inGigabytes + val gibibytes = subjectBinary.inGibibytes + val terabytes = subjectBinary.inTerabytes + val tebibytes = subjectBinary.inTebibytes + val petabytes = subjectBinary.inPetabytes + val pebibytes = subjectBinary.inPebibytes - assertEquals(0, bytes) + assertEquals(0, binaryBytes) + assertEquals(0, decimalBytes) assertEquals(0.0, kilobytes, 0.01) + assertEquals(0.0, kibibytes, 0.01) assertEquals(0.0, megabytes, 0.01) + assertEquals(0.0, mebibytes, 0.01) assertEquals(0.0, gigabytes, 0.01) + assertEquals(0.0, gibibytes, 0.01) assertEquals(0.0, terabytes, 0.01) + assertEquals(0.0, tebibytes, 0.01) + assertEquals(0.0, petabytes, 0.01) + assertEquals(0.0, pebibytes, 0.01) } @Test - fun `bytes convert to bytes`() { - val expectedBytes = 104857600L // 100 Mb - val subject = DataSize.convert( - expectedBytes.toDouble(), - DataSizeUnit.Bytes, - DataSizeUnit.Bytes, - ) - - val result = subject.bytes.inBytes - assertEquals(expectedBytes, result) - } - - @Test - fun `bytes convert to megabytes`() { + fun `toDataSize bytes keeps value`() { val expectedBytes = 104857600L // 100 Mb - val subject = DataSize.convert( - expectedBytes.toDouble(), - DataSizeUnit.Bytes, - DataSizeUnit.Megabytes, - ) - - val result = subject.megabytes.inBytes - assertEquals(expectedBytes, result) - } - - @Test - fun `megabytes convert to bytes`() { - val expectedBytes = 104857600L - val expectedBytesInMegabytes = 100.0 - val subject = DataSize.convert( - expectedBytesInMegabytes, - DataSizeUnit.Megabytes, - DataSizeUnit.Bytes, - ) - - val result = subject.bytes.inBytes - assertEquals(expectedBytes, result) - } - - @Test - fun `bytes express with toDataSize as bytes`() { - val expectedBytes = 104857600L // 100 Mb - val subject = expectedBytes.toDataSize(DataSizeUnit.Bytes) + val subject = expectedBytes.binary.toDataSize(BinaryUnit.Byte) val result = subject.inBytes assertEquals(expectedBytes, result) } @Test - fun `megabytes express with toDataSize as bytes`() { + fun `toDataSize mebibytes converts correctly`() { val expectedBytes = 123207680L val expectedMegaBytes = 117.5 - val subject = expectedMegaBytes.toDataSize(DataSizeUnit.Megabytes) + val subject = expectedMegaBytes.binary.toDataSize(BinaryUnit.Mebibyte) val resultBytes = subject.inBytes assertEquals(expectedBytes, resultBytes) - val resultMegabytes = subject.inMegabytes + val resultMegabytes = subject.inMebibytes assertEquals(expectedMegaBytes, resultMegabytes, 0.01) } @Test - fun `bytes convert with toDouble to kilobytes`() { + fun `bytes toDouble kibibytes converts correctly`() { val expectedBytes = 123207680L val expectedKilobytes = 120320.0 // 117.5 Mb - val subject = expectedBytes.bytes - val result = subject.toDouble(DataSizeUnit.Kilobytes) + val subject = expectedBytes.binary.bytes + val result = subject.toDouble(BinaryUnit.Kibibyte) assertEquals(expectedKilobytes, result, 0.01) } @Test - fun `bytes convert with toInt to megabytes`() { + fun `bytes toInt mebibytes truncates correctly`() { val expectedBytes = 123207680L val expectedMegabytes = 117 - val subject = expectedBytes.bytes - val result = subject.toInt(DataSizeUnit.Megabytes) + val subject = expectedBytes.binary.bytes + val result = subject.toInt(BinaryUnit.Mebibyte) assertEquals(expectedMegabytes, result) } @Test - fun `kilobytes convert with toLong to bytes`() { + fun `kibibytes toLong bytes converts correctly`() { val expectedBytes = 126164664320L val expectedKilobytes = 123207680L - val subject = expectedKilobytes.kilobytes - val result = subject.toLong(DataSizeUnit.Bytes) + val subject = expectedKilobytes.binary.kibibytes + val result = subject.toLong(BinaryUnit.Byte) assertEquals(expectedBytes, result) } @Test - fun `operator plus adds kilobytes`() { + fun `plus adds kibibytes correctly`() { val expectedBytes = 4096L - val subject = 2.kilobytes + 2.kilobytes + val subject = 2.binary.kibibytes + 2.binary.kibibytes val result = subject.inBytes assertEquals(expectedBytes, result) } @Test - fun `operator minus subtracts megabytes`() { + fun `minus subtracts mebibytes correctly`() { val expectedBytes = 2097152L - val subject = 4.megabytes - 2.megabytes + val subject = 4.binary.mebibytes - 2.binary.mebibytes val result = subject.inBytes assertEquals(expectedBytes, result) } @Test - fun `operator times whole number multiplication megabytes`() { + fun `times int multiplies megabytes`() { val expectedMegabytes = 20.0 - val subject = 2.megabytes * 10 + val subject = 2.decimal.megabytes * 10 val result = subject.inMegabytes assertEquals(expectedMegabytes, result, 0.01) } @Test - fun `operator times decimal point multiplication kilobytes`() { + fun `times decimal multiplies kilobytes`() { val expectedKilobytes = 3.0 - val subject = 2.kilobytes * 1.5 + val subject = 2.decimal.kilobytes * 1.5 val result = subject.inKilobytes assertEquals(expectedKilobytes, result, 0.01) } @Test - fun `operator div whole number divides kilobytes`() { + fun `div int divides kibibytes`() { val expectedKilobytes = 5.0 - val subject = 10.kilobytes / 2 + val subject = 10.binary.kibibytes / 2 - val result = subject.inKilobytes + val result = subject.inKibibytes assertEquals(expectedKilobytes, result, 0.01) } @Test - fun `operator div decimal point divides megabytes`() { + fun `div decimal divides megabytes`() { val expectedMegabytes = 7.33 - val subject = 11.megabytes / 1.5 + val subject = 11.decimal.megabytes / 1.5 val result = subject.inMegabytes assertEquals(expectedMegabytes, result, 0.01) } @Test - fun `operator div object divides bytes`() { - val expectedBytes = 10.24 - val result = 1.kilobytes / 100.bytes - - assertEquals(expectedBytes, result, 0.01) - } - - @Test - fun `operator div object by zero throws IllegalArgumentException`() { + fun `div by zero scalar throws`() { assertThrows(IllegalArgumentException::class.java) { - 1.kilobytes / 0 + 1.binary.kibibytes / 0 } assertThrows(IllegalArgumentException::class.java) { - 1.kilobytes / 0.0 + 1.decimal.kilobytes / 0 } - } - @Test - fun `operator div object by zero throws err if used ZERO`() { assertThrows(IllegalArgumentException::class.java) { - 1.kilobytes / DataSize.Zero + 1.binary.kibibytes / 0.0 + } + assertThrows(IllegalArgumentException::class.java) { + 1.decimal.kilobytes / 0.0 } } @Test - fun `operator div DataSizeUnit divided by DataSizeUnit`() { - val expected = 10_485.76 - val result = 1.terabytes / 100.megabytes + fun `compareTo orders correctly`() { + val expectedLess = -1 + val expectedEqual = 0 + val expectedGreater = 1 + + val lessResult = 123.binary.kibibytes.compareTo(123.binary.gibibytes) + val equalResult = 123.binary.mebibytes.compareTo(123.binary.mebibytes) + val greaterResult = 123.decimal.megabytes.compareTo(123.decimal.kilobytes) - assertEquals(expected, result, 0.01) + assertEquals(expectedLess, lessResult) + assertEquals(expectedEqual, equalResult) + assertEquals(expectedGreater, greaterResult) } @Test - fun `megabytes toString with DataSizeUnit no decimals`() { + fun `toString megabytes no decimals`() { val expected = "100 MB" - val result = 100.megabytes.toString(DataSizeUnit.Megabytes) + val result = 100.decimal.megabytes.toString(DecimalUnit.Megabyte) assertEquals(expected, result) } @Test - fun `megabytes toString with DataSizeUnit and decimals`() { + fun `toString mebibytes with decimals`() { val expected = "100,55 MB" - val result = 100.55.megabytes.toString(DataSizeUnit.Megabytes, decimals = 2) + val result = 100.55.binary.mebibytes.toString(BinaryUnit.Mebibyte, fractionDigits = 2) assertEquals(expected, result) } @Test - fun `megabytes toString with kilobytes DataSizeUnit no decimals`() { - val expected = "102.400 KB" - val result = 100.megabytes.toString(DataSizeUnit.Kilobytes) + fun `toString mebibytes as kibibytes`() { + val expected = "102 400 KB" + val result = 100.binary.mebibytes.toString(BinaryUnit.Kibibyte) assertEquals(expected, result) } @Test - fun `terabytes toString with gigabytes DataSizeUnit no decimals`() { + fun `toString tebibytes as gibibytes`() { val expected = "1024 GB" - val result = 1.terabytes.toString(DataSizeUnit.Gigabytes) + val result = 1.binary.tebibytes.toString(BinaryUnit.Gibibyte) assertEquals(expected, result) } @Test - @OptIn(ExperimentalDataSizeApi::class) - fun `megabytes toDecimalString with DataSizeUnit no decimals`() { + fun `toString mebibytes as decimal megabytes`() { val expected = "500 MB" - val result = 476.84.megabytes.toDecimalString(DataSizeUnit.Megabytes) + val result = 476.84.binary.mebibytes.toString(DecimalUnit.Megabyte) assertEquals(expected, result) } @Test - @OptIn(ExperimentalDataSizeApi::class) - fun `megabytes toDecimalString with DataSizeUnit and decimals`() { + fun `toString mebibytes as decimal megabytes with decimals`() { val expected = "512,5 MB" - val result = 488.755.megabytes.toDecimalString(DataSizeUnit.Megabytes, decimals = 1) + val result = 488.755.binary.mebibytes.toString(DecimalUnit.Megabyte, fractionDigits = 1) assertEquals(expected, result) } @Test - @OptIn(ExperimentalDataSizeApi::class) - fun `megabytes toDecimalString with kilobytes DataSizeUnit no decimals`() { + fun `toString mebibytes as decimal kilobytes`() { val expected = "1000 KB" - val result = 0.954.megabytes.toDecimalString(DataSizeUnit.Kilobytes) + val result = 0.954.binary.mebibytes.toString(DecimalUnit.Kilobyte) assertEquals(expected, result) } @Test - @OptIn(ExperimentalDataSizeApi::class) - fun `terabytes toDecimalString with gigabytes DataSizeUnit no decimals`() { + fun `toString tebibytes as decimal gigabytes`() { val expected = "255 GB" - val result = 0.232.terabytes.toDecimalString(DataSizeUnit.Gigabytes) + val result = 0.232.binary.tebibytes.toString(DecimalUnit.Gigabyte) assertEquals(expected, result) } @Test - fun `megabytes toBinaryUnit of megabytes`() { - val expected = 476.84 - val result = toBinaryUnit(500.0, DataSizeUnit.Megabytes) - - assertEquals(expected, result, 0.01) - } - - @Test - fun `gigabytes toBinaryUnit of terabytes`() { - val expected = 0.232 - val subject = toBinaryUnit(255, DataSizeUnit.Gigabytes).gigabytes - val result = subject.inTerabytes - - assertEquals(expected, result, 0.01) - } - - @Test - @OptIn(ExperimentalDataSizeApi::class) - fun `kilobytes toDecimalUnit plus megabytes toBinaryUnit`() { - val expected = 2_000_000L - val subjectKilobytes = - toBinaryUnit( - // technically possible, but requires more precision. - toDecimalUnit(976.5621, DataSizeUnit.Kilobytes), - DataSizeUnit.Kilobytes, - ).kilobytes - val subjectMegabytes = toBinaryUnit(1, DataSizeUnit.Megabytes).megabytes - val result = subjectKilobytes + subjectMegabytes + fun `ByteArray converts to DataSize correctly`() { + val expected = "50 MB" + val result = ByteArray(50 * 1024 * 1024).binary.bytes.toString(BinaryUnit.Mebibyte) - assertEquals(expected, result.inBytes) - } - - @Test - fun `compareTo returns correct value`() { - val expectedLess = -1 - val expectedEqual = 0 - val expectedGreater = 1 - - val lessResult = 123.kilobytes.compareTo(123.gigabytes) - val equalResult = 123.megabytes.compareTo(123.megabytes) - val greaterResult = 123.megabytes.compareTo(123.kilobytes) - - assertEquals(expectedLess, lessResult) - assertEquals(expectedEqual, equalResult) - assertEquals(expectedGreater, greaterResult) + assertEquals(expected, result) } @Test - fun `show correct decimalSeparator`() { + fun `decimal separator formats correctly`() { val expected = "1,1 MB" - val result = 1.1.megabytes.toString(unit = DataSizeUnit.Megabytes, decimals = 1) + val result = 1.1.binary.mebibytes.toString(unit = BinaryUnit.Mebibyte, fractionDigits = 1) assertEquals(expected, result) } @Test - fun `show correct groupingSeparator`() { - val expected = "10.000 MB" - val result = 10_000.megabytes.toString(unit = DataSizeUnit.Megabytes, decimals = 1) + fun `grouping separator formats correctly`() { + val expected = "10 000 MB" + val result = 10_000.binary.mebibytes.toString(unit = BinaryUnit.Mebibyte, fractionDigits = 1) assertEquals(expected, result) } @Test - fun `show correct groupingSeparator with floating point`() { - val expected = "100.000,5 MB" - val result = 100_000.5.megabytes.toString(unit = DataSizeUnit.Megabytes, decimals = 1) + fun `grouping with decimals formats correctly`() { + val expected = "100 000,5 MB" + val result = 100_000.5.binary.mebibytes.toString(unit = BinaryUnit.Mebibyte, fractionDigits = 1) assertEquals(expected, result) } @Test - fun `hide groupingSeparator when groupingSize less then 3`() { + fun `no grouping when size less than three`() { val expected = "1000 MB" - val result = 1000.megabytes.toString(unit = DataSizeUnit.Megabytes, decimals = 1) + val result = 1000.binary.mebibytes.toString(unit = BinaryUnit.Mebibyte, fractionDigits = 1) assertEquals(expected, result) } @Test - fun `orZero returns 0 bytes when value is null`() { + fun `orBinaryZero returns zero bytes for null`() { + val x: DataSize? = null + assertEquals(0, x.orBinaryZero().inBytes) + } + + @Test + fun `orDecimalZero returns zero kilobytes for null`() { val x: DataSize? = null - assertEquals(0, x.orZero().inBytes) + assertEquals(0.0, x.orDecimalZero().inKilobytes, 0.0) } @Test - fun `orZero returns 0 kilobytes when value is null`() { + fun `orBinaryZero returns zero mebibytes for null`() { val x: DataSize? = null - assertEquals(0.0, x.orZero().inKilobytes, 0.0) + assertEquals(0.0, x.orBinaryZero().inMebibytes, 0.0) } @Test - fun `orZero returns 0 megabytes when value is null`() { + fun `orDecimalZero returns zero gigabytes for null`() { val x: DataSize? = null - assertEquals(0.0, x.orZero().inMegabytes, 0.0) + assertEquals(0.0, x.orDecimalZero().inGigabytes, 0.0) } @Test - fun `orZero returns 0 gigabytes when value is null`() { + fun `orBinaryZero returns zero terabytes for null`() { val x: DataSize? = null - assertEquals(0.0, x.orZero().inGigabytes, 0.0) + assertEquals(0.0, x.orBinaryZero().inTerabytes, 0.0) } @Test - fun `orZero returns 0 terabytes when value is null`() { + fun `orDecimalZero returns zero petabytes for null`() { val x: DataSize? = null - assertEquals(0.0, x.orZero().inTerabytes, 0.0) + assertEquals(0.0, x.orDecimalZero().inPetabytes, 0.0) } } \ No newline at end of file diff --git a/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DefaultDataSizeFormatterTest.kt b/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DefaultDataSizeFormatterTest.kt new file mode 100644 index 0000000..d5c5c11 --- /dev/null +++ b/datasize-core/src/test/kotlin/io/github/ardiien/datasize/DefaultDataSizeFormatterTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2026 ardiien + * Licensed under the Apache License, Version 2.0. + * See http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.github.ardiien.datasize + +import io.github.ardiien.datasize.DataSize.Companion.binary +import io.github.ardiien.datasize.formatter.DefaultDataSizeFormatter +import io.github.ardiien.datasize.unit.BinaryUnit +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + + +class DefaultDataSizeFormatterTest { + + private val formatter: DefaultDataSizeFormatter = DefaultDataSizeFormatter(DefaultDataSizeFormatter.createFormat()) + + @Test + fun `bytes returns Byte unit`() { + val expected = BinaryUnit.Byte + + val subject = 500.binary.bytes + val actual = formatter.unitFrom(subject) + + assertEquals(expected, actual) + } + + @Test + fun `kibibytes returns Kibibyte unit`() { + val expected = BinaryUnit.Kibibyte + + val subject = 500.binary.kibibytes + val actual = formatter.unitFrom(subject) + + assertEquals(expected, actual) + } + + @Test + fun `mebibytes returns Mebibyte unit`() { + val expected = BinaryUnit.Mebibyte + + val subject = 500.binary.mebibytes + val actual = formatter.unitFrom(subject) + + assertEquals(expected, actual) + } + + @Test + fun `gibibytes returns Gibibyte unit`() { + val expected = BinaryUnit.Gibibyte + + val subject = 500.binary.gibibytes + val actual = formatter.unitFrom(subject) + + assertEquals(expected, actual) + } + + @Test + fun `tebibytes returns Tebibyte unit`() { + val expected = BinaryUnit.Tebibyte + + val subject = 500.binary.tebibytes + val actual = formatter.unitFrom(subject) + + assertEquals(expected, actual) + } + + @Test + fun `pebibytes returns Pebibyte unit`() { + val expected = BinaryUnit.Pebibyte + + val subject = 500.binary.pebibytes + val actual = formatter.unitFrom(subject) + + assertEquals(expected, actual) + } + + @Test + fun `format with zero fraction digits returns correct string`() { + val expected = "5 GB" + + val size = 5.32.binary.gibibytes + val actual = formatter.format( + value = size, + unit = BinaryUnit.Gibibyte, + fractionDigits = 0, + ) + + assertEquals(expected, actual) + } + + @Test + fun `format with one fraction digit returns correct string`() { + val expected = "5,3 GB" + + val size = 5.32.binary.gibibytes + val actual = formatter.format( + value = size, + unit = BinaryUnit.Gibibyte, + fractionDigits = 1, + ) + + assertEquals(expected, actual) + } + + @Test + fun `format with two fraction digits returns correct string`() { + val expected = "5,32 GB" + + val size = 5.32.binary.gibibytes + val actual = formatter.format( + value = size, + unit = BinaryUnit.Gibibyte, + fractionDigits = 2, + ) + + assertEquals(expected, actual) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5930f5..90d4622 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,11 +2,13 @@ bcv = "0.18.1" dokka = "2.1.0" kotlin = "2.3.0" +kotlinCollections = "0.4.0" shadow = "8.3.9" publish = "0.36.0" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +kotlinx-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinCollections" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } [plugins] diff --git a/samples/src/main/kotlin/sample-basic-01.kt b/samples/src/main/kotlin/sample-basic-01.kt index 5b75488..f96f9c6 100644 --- a/samples/src/main/kotlin/sample-basic-01.kt +++ b/samples/src/main/kotlin/sample-basic-01.kt @@ -1,10 +1,11 @@ -import io.github.ardiien.datasize.DataSize.Companion.kilobytes +import io.github.ardiien.datasize.DataSize.Companion.binary +import io.github.ardiien.datasize.DataSize.Companion.decimal fun main() { - val kilobyteFromInt = 1.kilobytes - val kilobyteFromDouble = 1.0.kilobytes - val kilobyteFromLong = 1L.kilobytes + val kilobyteFromInt = 1.binary.kibibytes + val kilobyteFromDouble = 1.0.decimal.kilobytes + val kilobyteFromLong = 1L.binary.kibibytes println(kilobyteFromInt.inBytes) println(kilobyteFromDouble.inBytes) diff --git a/samples/src/main/kotlin/sample-basic-02.kt b/samples/src/main/kotlin/sample-basic-02.kt index bd33af6..06b318a 100644 --- a/samples/src/main/kotlin/sample-basic-02.kt +++ b/samples/src/main/kotlin/sample-basic-02.kt @@ -1,19 +1,21 @@ -import io.github.ardiien.datasize.DataSize.Companion.megabytes -import io.github.ardiien.datasize.DataSizeUnit +import io.github.ardiien.datasize.DataSize.Companion.binary +import io.github.ardiien.datasize.DataSize.Companion.decimal +import io.github.ardiien.datasize.unit.BinaryUnit +import io.github.ardiien.datasize.unit.DecimalUnit fun main() { - val addition = 5.megabytes + 15.megabytes - val substraction = 105.megabytes - 5.megabytes + val addition = 5.binary.mebibytes + 15.binary.mebibytes + val substraction = 105.decimal.megabytes - 5.decimal.megabytes - val multiplication = 5.megabytes * 2 - val division = 15.megabytes / 2 - val remainder = 11.megabytes % 2.megabytes + val multiplication = 5.decimal.megabytes * 2 + val division = 15.binary.mebibytes / 2 + val remainder = 11.decimal.megabytes % 2.decimal.megabytes - println(addition.toString(DataSizeUnit.Megabytes, decimals = 1)) - println(substraction.toString(DataSizeUnit.Megabytes, decimals = 1)) + println(addition.toString(BinaryUnit.Mebibyte, fractionDigits = 1)) + println(substraction.toString(DecimalUnit.Megabyte, fractionDigits = 1)) - println(multiplication.toString(DataSizeUnit.Megabytes, decimals = 1)) - println(division.toString(DataSizeUnit.Megabytes, decimals = 1)) - println(remainder.toString(DataSizeUnit.Megabytes, decimals = 1)) + println(multiplication.toString(DecimalUnit.Megabyte, fractionDigits = 1)) + println(division.toString(BinaryUnit.Mebibyte, fractionDigits = 1)) + println(remainder.toString(DecimalUnit.Megabyte, fractionDigits = 1)) } \ No newline at end of file diff --git a/samples/src/main/kotlin/sample-basic-03.kt b/samples/src/main/kotlin/sample-basic-03.kt index e927bf7..e72ab45 100644 --- a/samples/src/main/kotlin/sample-basic-03.kt +++ b/samples/src/main/kotlin/sample-basic-03.kt @@ -1,21 +1,20 @@ -import io.github.ardiien.datasize.DataSize.Companion.kilobytes -import io.github.ardiien.datasize.DataSize.Companion.megabytes +import io.github.ardiien.datasize.DataSize.Companion.binary import io.github.ardiien.datasize.max import io.github.ardiien.datasize.min fun main() { val sortedList = listOf( - 1.kilobytes, 1.megabytes, 20.kilobytes + 1.binary.kibibytes, 1.binary.mebibytes, 20.binary.kibibytes ).sorted() - val gt = 15.kilobytes > 1.kilobytes - val lte = 15.kilobytes <= 14.kilobytes - val eq = 15.kilobytes == 15.kilobytes - val neq = 15.kilobytes != 5.kilobytes + val gt = 15.binary.kibibytes > 1.binary.kibibytes + val lte = 15.binary.kibibytes <= 14.binary.kibibytes + val eq = 15.binary.kibibytes == 15.binary.kibibytes + val neq = 15.binary.kibibytes != 5.binary.kibibytes - val min = min(2.megabytes, 2.kilobytes) - val max = max(2.megabytes, 2.kilobytes) + val min = min(2.binary.mebibytes, 2.binary.kibibytes) + val max = max(2.binary.mebibytes, 2.binary.kibibytes) println(sortedList) diff --git a/samples/src/main/kotlin/sample-basic-04.kt b/samples/src/main/kotlin/sample-basic-04.kt index f8150b1..bae7ee0 100644 --- a/samples/src/main/kotlin/sample-basic-04.kt +++ b/samples/src/main/kotlin/sample-basic-04.kt @@ -1,13 +1,14 @@ -import io.github.ardiien.datasize.DataSize.Companion.kilobytes -import io.github.ardiien.datasize.DataSizeFormatter +import io.github.ardiien.datasize.DataSize.Companion.binary +import io.github.ardiien.datasize.formatter.DefaultDataSizeFormatter fun main() { - val value = 55.563.kilobytes + val formatter = DefaultDataSizeFormatter(DefaultDataSizeFormatter.createFormat()) + val value = 55.563.binary.kibibytes - val defaultPrecision = DataSizeFormatter.format(value, decimals = 0) - val betterPrecision = DataSizeFormatter.format(value, decimals = 1) - val maxAvailablePrecision = DataSizeFormatter.format(value, decimals = 2) + val defaultPrecision = formatter.format(value, fractionDigits = 0) + val betterPrecision = formatter.format(value, fractionDigits = 1) + val maxAvailablePrecision = formatter.format(value, fractionDigits = 2) println(defaultPrecision) println(betterPrecision)