├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── fraction.zig └── fraction_test.zig /.gitignore: -------------------------------------------------------------------------------- 1 | /.zig-cache 2 | /zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Christopher Rybicki 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-fractions 2 | 3 | A Zig library for performing math with fractions, where each fraction is represented by a pair of integers. 4 | 5 | Compatible with Zig 0.13 stable. 6 | 7 | Supported APIs: 8 | 9 | - convertion from floats, or numerator/denominator pairs (`fromFloat()`, `init()`) 10 | - convertion to floats and integers (`to(T)`) 11 | - formatting (`toString()`, `toStringAlloc()`, `format()`) 12 | - negation, absolute value (`negate()`, `abs()`) 13 | - addition, subtraction, multiplication, division (`add()`, `sub()`, `mul()`, `div()`) 14 | - comparison (`eql()`, `eqlAbs()`, `eqlZero()`, `order()`, `orderAbs()`) 15 | - simplifying (`simplify()`) 16 | 17 | ## Installation 18 | 19 | First, run the following: 20 | 21 | ``` 22 | zig fetch --save git+https://github.com/Chriscbr/zig-fractions 23 | ``` 24 | 25 | Then add the following to build.zig: 26 | 27 | ```zig 28 | const zig_fractions = b.dependency("zig-fractions", .{}); 29 | exe.root_module.addImport("zig-fractions", zig_fractions.module("zig-fractions")); 30 | ``` 31 | 32 | Then you can use the library in your Zig project: 33 | 34 | ```zig 35 | const Fraction = @import("zig-fractions").Fraction; 36 | 37 | var f1 = try Fraction.fromFloat(@as(f32, 2.5)); 38 | const f2 = try Fraction.init(1, 5, false); 39 | try f1.mul(&f2); // 2.5 * 1/5 = 1/2 40 | std.debug.print("{}\n", .{f1}); // "1/2" 41 | ``` 42 | 43 | ## Contributing 44 | 45 | Pull requests are welcome. 46 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | // Standard optimization options allow the person running `zig build` to select 14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 15 | // set a preferred release mode, allowing the user to decide how to optimize. 16 | const optimize = b.standardOptimizeOption(.{}); 17 | 18 | const lib = b.addStaticLibrary(.{ 19 | .name = "zig-fractions", 20 | // In this case the main source file is merely a path, however, in more 21 | // complicated build scripts, this could be a generated file. 22 | .root_source_file = b.path("src/fraction.zig"), 23 | .target = target, 24 | .optimize = optimize, 25 | }); 26 | 27 | // This declares intent for the library to be installed into the standard 28 | // location when the user invokes the "install" step (the default step when 29 | // running `zig build`). 30 | b.installArtifact(lib); 31 | 32 | // const exe = b.addExecutable(.{ 33 | // .name = "zig-fractions", 34 | // .root_source_file = b.path("src/main.zig"), 35 | // .target = target, 36 | // .optimize = optimize, 37 | // }); 38 | 39 | // // This declares intent for the executable to be installed into the 40 | // // standard location when the user invokes the "install" step (the default 41 | // // step when running `zig build`). 42 | // b.installArtifact(exe); 43 | 44 | // // This *creates* a Run step in the build graph, to be executed when another 45 | // // step is evaluated that depends on it. The next line below will establish 46 | // // such a dependency. 47 | // const run_cmd = b.addRunArtifact(exe); 48 | 49 | // // By making the run step depend on the install step, it will be run from the 50 | // // installation directory rather than directly from within the cache directory. 51 | // // This is not necessary, however, if the application depends on other installed 52 | // // files, this ensures they will be present and in the expected location. 53 | // run_cmd.step.dependOn(b.getInstallStep()); 54 | 55 | // // This allows the user to pass arguments to the application in the build 56 | // // command itself, like this: `zig build run -- arg1 arg2 etc` 57 | // if (b.args) |args| { 58 | // run_cmd.addArgs(args); 59 | // } 60 | 61 | // // This creates a build step. It will be visible in the `zig build --help` menu, 62 | // // and can be selected like this: `zig build run` 63 | // // This will evaluate the `run` step rather than the default, which is "install". 64 | // const run_step = b.step("run", "Run the app"); 65 | // run_step.dependOn(&run_cmd.step); 66 | 67 | // Creates a step for unit testing. This only builds the test executable 68 | // but does not run it. 69 | const lib_unit_tests = b.addTest(.{ 70 | .root_source_file = b.path("src/fraction.zig"), 71 | .target = target, 72 | .optimize = optimize, 73 | }); 74 | 75 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 76 | 77 | // const exe_unit_tests = b.addTest(.{ 78 | // .root_source_file = b.path("src/main.zig"), 79 | // .target = target, 80 | // .optimize = optimize, 81 | // }); 82 | 83 | // const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); 84 | 85 | // Similar to creating the run step earlier, this exposes a `test` step to 86 | // the `zig build --help` menu, providing a way for the user to request 87 | // running the unit tests. 88 | const test_step = b.step("test", "Run unit tests"); 89 | test_step.dependOn(&run_lib_unit_tests.step); 90 | // test_step.dependOn(&run_exe_unit_tests.step); 91 | 92 | _ = b.addModule("zig-fractions", .{ .root_source_file = b.path("src/fraction.zig") }); 93 | } 94 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | // This is the default name used by packages depending on this one. For 3 | // example, when a user runs `zig fetch --save `, this field is used 4 | // as the key in the `dependencies` table. Although the user can choose a 5 | // different name, most users will stick with this provided value. 6 | // 7 | // It is redundant to include "zig" in this name because it is already 8 | // within the Zig package namespace. 9 | .name = "zig-fractions", 10 | 11 | // This is a [Semantic Version](https://semver.org/). 12 | // In a future version of Zig it will be used for package deduplication. 13 | .version = "0.0.0", 14 | 15 | // This field is optional. 16 | // This is currently advisory only; Zig does not yet do anything 17 | // with this value. 18 | //.minimum_zig_version = "0.11.0", 19 | 20 | // This field is optional. 21 | // Each dependency must either provide a `url` and `hash`, or a `path`. 22 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 23 | // Once all dependencies are fetched, `zig build` no longer requires 24 | // internet connectivity. 25 | .dependencies = .{ 26 | // See `zig fetch --save ` for a command-line interface for adding dependencies. 27 | //.example = .{ 28 | // // When updating this field to a new URL, be sure to delete the corresponding 29 | // // `hash`, otherwise you are communicating that you expect to find the old hash at 30 | // // the new URL. 31 | // .url = "https://example.com/foo.tar.gz", 32 | // 33 | // // This is computed from the file contents of the directory of files that is 34 | // // obtained after fetching `url` and applying the inclusion rules given by 35 | // // `paths`. 36 | // // 37 | // // This field is the source of truth; packages do not come from a `url`; they 38 | // // come from a `hash`. `url` is just one of many possible mirrors for how to 39 | // // obtain a package matching this `hash`. 40 | // // 41 | // // Uses the [multihash](https://multiformats.io/multihash/) format. 42 | // .hash = "...", 43 | // 44 | // // When this is provided, the package is found in a directory relative to the 45 | // // build root. In this case the package's hash is irrelevant and therefore not 46 | // // computed. This field and `url` are mutually exclusive. 47 | // .path = "foo", 48 | 49 | // // When this is set to `true`, a package is declared to be lazily 50 | // // fetched. This makes the dependency only get fetched if it is 51 | // // actually used. 52 | // .lazy = false, 53 | //}, 54 | }, 55 | 56 | // Specifies the set of files and directories that are included in this package. 57 | // Only files and directories listed here are included in the `hash` that 58 | // is computed for this package. Only files listed here will remain on disk 59 | // when using the zig package manager. As a rule of thumb, one should list 60 | // files required for compilation plus any license(s). 61 | // Paths are relative to the build root. Use the empty string (`""`) to refer to 62 | // the build root itself. 63 | // A directory listed here means that all files within, recursively, are included. 64 | .paths = .{ 65 | "build.zig", 66 | "build.zig.zon", 67 | "src", 68 | "LICENSE", 69 | "README.md", 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /src/fraction.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const math = std.math; 3 | const assert = std.debug.assert; 4 | const Allocator = std.mem.Allocator; 5 | 6 | pub const FractionError = error{ 7 | DenominatorCannotBeZero, 8 | DivisionByZero, 9 | CannotConvertFromFloat, 10 | FractionIsNotInteger, 11 | FractionIsNegative, 12 | FractionOutsideTargetRange, 13 | }; 14 | 15 | // TODO: mutating floor(), ceil(), round() 16 | // TODO: toFloor(), toCeil(), toRound() 17 | // TODO: sqrt(), toSqrt() 18 | 19 | pub const Fraction = struct { 20 | /// The numerator of the fraction. 21 | num: usize, 22 | 23 | /// The denominator of the fraction. 24 | denom: usize, 25 | 26 | /// The sign bit of the fraction. True if the fraction is negative. 27 | sign: bool, 28 | 29 | /// Create a fraction from a float. 30 | /// The float must not be NaN or infinity. 31 | pub fn fromFloat(value: anytype) !Fraction { 32 | // Based on: https://github.com/python/cpython/blob/6239d41527d5977aa5d44e4b894d719bc045860e/Objects/floatobject.c#L1556 33 | if (math.isNan(value)) { 34 | return FractionError.CannotConvertFromFloat; 35 | } 36 | if (math.isInf(value)) { 37 | return FractionError.CannotConvertFromFloat; 38 | } 39 | if (value == 0) { 40 | return Fraction{ .num = 0, .denom = 1, .sign = false }; 41 | } 42 | 43 | const f = math.frexp(value); 44 | const sign = f.significand < 0; 45 | var float_part = @abs(f.significand); 46 | var exponent = f.exponent; 47 | 48 | var i: u16 = 0; 49 | while (i < 300 and float_part != math.floor(float_part)) : (i += 1) { 50 | float_part *= 2; 51 | exponent -= 1; 52 | } 53 | var num: usize = @intFromFloat(float_part); 54 | var denom: usize = 1; 55 | if (exponent > 0) { 56 | num <<= @intCast(@abs(exponent)); 57 | } else { 58 | denom <<= @intCast(@abs(exponent)); 59 | } 60 | 61 | var frac = Fraction{ .num = num, .denom = denom, .sign = sign }; 62 | frac.simplify(); 63 | return frac; 64 | } 65 | 66 | /// Initialize the fraction. The sign bit is true if the fraction is negative. 67 | /// The denominator must not be zero. 68 | pub fn init(num: usize, denom: usize, sign: bool) !Fraction { 69 | if (denom == 0) { 70 | return FractionError.DenominatorCannotBeZero; 71 | } 72 | return Fraction{ .num = num, .denom = denom, .sign = sign }; 73 | } 74 | 75 | /// Returns the length of the string representation of the fraction. 76 | pub fn toStringBufferLen(self: *const Fraction) usize { 77 | var len: usize = 0; 78 | if (self.sign) { 79 | len += 1; 80 | } 81 | var num = self.num; 82 | if (num == 0) { 83 | len += 1; 84 | } else { 85 | while (num != 0) { 86 | len += 1; 87 | num /= 10; 88 | } 89 | } 90 | len += 1; // '/' 91 | var denom = self.denom; 92 | while (denom != 0) { 93 | len += 1; 94 | denom /= 10; 95 | } 96 | return len; 97 | } 98 | 99 | /// Converts the fraction to a string. 100 | /// Caller owns returned memory. 101 | /// See also `toString`, a lower level function than this. 102 | pub fn toStringAlloc(self: *const Fraction, allocator: Allocator, case: std.fmt.Case) Allocator.Error![]u8 { 103 | const string = try allocator.alloc(u8, self.toStringBufferLen()); 104 | errdefer allocator.free(string); 105 | return allocator.realloc(string, self.toString(string, case)); 106 | } 107 | 108 | /// Return the string representation of the fraction. 109 | /// `buf` is caller-provided memory for toString to use as a working area. 110 | /// It must have length at least `toStringBufferLen`. 111 | /// Returns the length of the string. 112 | /// See also `toStringAlloc`, a higher level function than this. 113 | pub fn toString(self: *const Fraction, buf: []u8, case: std.fmt.Case) usize { 114 | assert(self.denom != 0); 115 | var digits_len: usize = 0; 116 | 117 | if (self.sign) { 118 | buf[digits_len] = '-'; 119 | digits_len += 1; 120 | } 121 | 122 | var num = self.num; 123 | if (num == 0) { 124 | buf[digits_len] = '0'; 125 | digits_len += 1; 126 | } else { 127 | while (num != 0) { 128 | const digit: u8 = @intCast(num % 10); 129 | const ch = std.fmt.digitToChar(digit, case); 130 | buf[digits_len] = ch; 131 | digits_len += 1; 132 | num /= 10; 133 | } 134 | std.mem.reverse(u8, buf[@intFromBool(self.sign)..digits_len]); 135 | } 136 | 137 | buf[digits_len] = '/'; 138 | digits_len += 1; 139 | 140 | var denom = self.denom; 141 | const denom_start = digits_len; 142 | while (denom != 0) { 143 | const digit: u8 = @intCast(denom % 10); 144 | const ch = std.fmt.digitToChar(digit, case); 145 | buf[digits_len] = ch; 146 | digits_len += 1; 147 | denom /= 10; 148 | } 149 | std.mem.reverse(u8, buf[denom_start..digits_len]); 150 | 151 | return digits_len; 152 | } 153 | 154 | /// To allow `std.fmt.format` to work with this type. 155 | pub fn format( 156 | self: *const Fraction, 157 | comptime fmt: []const u8, 158 | options: std.fmt.FormatOptions, 159 | out_stream: anytype, 160 | ) !void { 161 | _ = options; 162 | const case = std.fmt.Case.lower; 163 | 164 | if (fmt.len == 0 or comptime std.mem.eql(u8, fmt, "d")) { 165 | // format as decimal 166 | } else { 167 | std.fmt.invalidFmtError(fmt, self); 168 | } 169 | 170 | // A usize can be at most max(u64) = 18446744073709551615 = 20 digits 171 | // So the maximum length of the string representation of a fraction is 172 | // 1 (sign) + 20 + 1 (slash) + 20 = 42 173 | var buf: [42]u8 = undefined; 174 | const len = self.toString(&buf, case); 175 | return out_stream.writeAll(buf[0..len]); 176 | } 177 | 178 | /// Modify to become the absolute value. 179 | pub fn abs(self: *Fraction) void { 180 | if (self.sign == true) { 181 | self.sign = false; 182 | } 183 | } 184 | 185 | /// Return a new fraction that is the absolute value. 186 | pub fn toAbs(self: Fraction) Fraction { 187 | return Fraction{ .num = self.num, .denom = self.denom, .sign = false }; 188 | } 189 | 190 | /// Modify to become the negative. 191 | pub fn negate(self: *Fraction) void { 192 | self.sign = !self.sign; 193 | } 194 | 195 | /// Return a new fraction that is the negative. 196 | pub fn toNegation(self: *const Fraction) Fraction { 197 | return Fraction{ .num = self.num, .denom = self.denom, .sign = !self.sign }; 198 | } 199 | 200 | /// Modify to become the reciprocal. 201 | pub fn reciprocal(self: *Fraction) !void { 202 | const tmp = self.num; 203 | self.num = self.denom; 204 | self.denom = tmp; 205 | if (self.denom == 0) { 206 | return FractionError.DenominatorCannotBeZero; 207 | } 208 | } 209 | 210 | /// Return a new fraction that is the reciprocal. 211 | pub fn toReciprocal(self: Fraction) !Fraction { 212 | if (self.num == 0) { 213 | return FractionError.DenominatorCannotBeZero; 214 | } 215 | return Fraction{ .num = self.denom, .denom = self.num, .sign = self.sign }; 216 | } 217 | 218 | /// Returns true if the two fractions are equal. 219 | pub fn eql(a: *const Fraction, b: *const Fraction) !bool { 220 | const r1 = try math.mul(usize, a.num, b.denom); 221 | const r2 = try math.mul(usize, b.num, a.denom); 222 | return a.sign == b.sign and r1 == r2; 223 | } 224 | 225 | /// Returns true if the two fractions are equal in absolute value. 226 | pub fn eqlAbs(a: *const Fraction, b: *const Fraction) !bool { 227 | const r1 = try math.mul(usize, a.num, b.denom); 228 | const r2 = try math.mul(usize, b.num, a.denom); 229 | return r1 == r2; 230 | } 231 | 232 | /// Returns true if the fraction is zero. 233 | pub fn eqlZero(self: *const Fraction) bool { 234 | return self.num == 0; 235 | } 236 | 237 | /// Simplify the fraction. 238 | pub fn simplify(self: *Fraction) void { 239 | const gcd = math.gcd(self.num, self.denom); 240 | self.num = @divExact(self.num, gcd); 241 | self.denom = @divExact(self.denom, gcd); 242 | } 243 | 244 | /// Return a new fraction that is the simplified version. 245 | pub fn toSimplified(self: *const Fraction) Fraction { 246 | const gcd = math.gcd(self.num, self.denom); 247 | return Fraction{ .num = @divExact(self.num, gcd), .denom = @divExact(self.denom, gcd), .sign = self.sign }; 248 | } 249 | 250 | /// Returns `math.Order.lt`, `math.Order.eq`, `math.Order.gt` if `a < b`, `a == b` or `a > b` respectively. 251 | pub fn order(self: *const Fraction, other: *const Fraction) !math.Order { 252 | if (self.sign != other.sign) { 253 | if (eqlZero(self) and eqlZero(other)) { 254 | return .eq; 255 | } else { 256 | return if (self.sign) .lt else .gt; 257 | } 258 | } else { 259 | const r = try orderAbs(self, other); 260 | return if (!self.sign) r else switch (r) { 261 | .lt => math.Order.gt, 262 | .eq => math.Order.eq, 263 | .gt => math.Order.lt, 264 | }; 265 | } 266 | } 267 | 268 | /// Returns `math.Order.lt`, `math.Order.eq`, `math.Order.gt` if 269 | /// `|a| < |b|`, `|a| == |b|`, or `|a| > |b|` respectively. 270 | pub fn orderAbs(self: *const Fraction, other: *const Fraction) !math.Order { 271 | // a/b < c/d <=> a*d < b*c 272 | const ad = try math.mul(usize, self.num, other.denom); 273 | const bc = try math.mul(usize, other.num, self.denom); 274 | return math.order(ad, bc); 275 | } 276 | 277 | /// Mutates this fraction to round down the value to the nearest integer. 278 | pub fn floor(self: *Fraction) void { 279 | self.num = @divFloor(self.num, self.denom); 280 | self.denom = 1; 281 | self.simplify(); 282 | } 283 | 284 | /// Add another fraction to this fraction. 285 | /// The result is stored in this fraction. 286 | pub fn add(self: *Fraction, other: *const Fraction) !void { 287 | const ad = try math.mul(usize, self.num, other.denom); 288 | const bc = try math.mul(usize, other.num, self.denom); 289 | const denom = try math.mul(usize, self.denom, other.denom); 290 | 291 | if (self.sign == other.sign) { 292 | // a/b + c/d = (a*d + b*c) / b*d 293 | const num = try math.add(usize, ad, bc); 294 | self.num = num; 295 | self.denom = denom; 296 | self.simplify(); 297 | return; 298 | } 299 | 300 | const ord = try self.orderAbs(other); 301 | switch (ord) { 302 | .eq => { 303 | self.num = 0; 304 | self.denom = 1; 305 | self.sign = false; 306 | }, 307 | .gt => { 308 | // a/b + -c/d = (a*d - b*c) / b*d 309 | const num = try math.sub(usize, ad, bc); 310 | self.num = num; 311 | self.denom = denom; 312 | self.sign = self.sign; 313 | self.simplify(); 314 | }, 315 | .lt => { 316 | // a/b + -c/d = - (b*c - a*d) / b*d 317 | const num = try math.sub(usize, bc, ad); 318 | self.num = num; 319 | self.denom = denom; 320 | self.sign = !self.sign; 321 | self.simplify(); 322 | }, 323 | } 324 | } 325 | 326 | /// Subtract another fraction from this fraction. 327 | /// The result is stored in this fraction. 328 | pub fn sub(self: *Fraction, other: *const Fraction) !void { 329 | const ord = try self.orderAbs(other); 330 | if (ord == .eq) { 331 | self.num = 0; 332 | self.denom = 1; 333 | self.sign = false; 334 | return; 335 | } 336 | 337 | const ad = try math.mul(usize, self.num, other.denom); 338 | const bc = try math.mul(usize, other.num, self.denom); 339 | const denom = try math.mul(usize, self.denom, other.denom); 340 | 341 | if (self.sign != other.sign) { 342 | // a/b - -c/d = (a*d + b*c) / b*d 343 | const num = try math.add(usize, ad, bc); 344 | self.num = num; 345 | self.denom = denom; 346 | self.simplify(); 347 | return; 348 | } 349 | 350 | switch (ord) { 351 | .gt => { 352 | // a/b - c/d = (a*d - b*c) / b*d 353 | const num = try math.sub(usize, ad, bc); 354 | self.num = num; 355 | self.denom = denom; 356 | self.sign = self.sign; 357 | self.simplify(); 358 | }, 359 | .lt => { 360 | // a/b - c/d = - (b*c - a*d) / b*d 361 | const num = try math.sub(usize, bc, ad); 362 | self.num = num; 363 | self.denom = denom; 364 | self.sign = !self.sign; 365 | self.simplify(); 366 | }, 367 | .eq => unreachable, 368 | } 369 | } 370 | 371 | /// Multiply another fraction to this fraction. 372 | /// The result is stored in this fraction. 373 | pub fn mul(self: *Fraction, other: *const Fraction) !void { 374 | // a/b * c/d = a*c / b*d 375 | const num = try math.mul(usize, self.num, other.num); 376 | const denom = try math.mul(usize, self.denom, other.denom); 377 | self.num = num; 378 | self.denom = denom; 379 | self.sign = self.sign != other.sign; 380 | self.simplify(); 381 | } 382 | 383 | /// Divide this fraction by another fraction. 384 | /// The result is stored in this fraction. 385 | pub fn div(self: *Fraction, other: *const Fraction) !void { 386 | if (other.num == 0) { 387 | return FractionError.DivisionByZero; 388 | } 389 | // a/b / c/d = a/b * d/c = a*d / b*c 390 | const num = try math.mul(usize, self.num, other.denom); 391 | const denom = try math.mul(usize, self.denom, other.num); 392 | self.num = num; 393 | self.denom = denom; 394 | self.sign = self.sign != other.sign; 395 | self.simplify(); 396 | } 397 | 398 | /// Convert self to type T. 399 | /// 400 | /// Returns an error if self cannot be narrowed into the requested type without truncation. 401 | pub fn to(self: *const Fraction, comptime T: type) !T { 402 | switch (@typeInfo(T)) { 403 | .Float => { 404 | const sign = @as(T, if (self.sign) -1.0 else 1.0); 405 | return sign * @as(T, @floatFromInt(self.num)) / @as(T, @floatFromInt(self.denom)); 406 | }, 407 | .Int => { 408 | const signedness = @typeInfo(T).Int.signedness; 409 | if (self.sign and signedness == .unsigned) { 410 | return FractionError.FractionIsNegative; 411 | } 412 | const remainder = self.num % self.denom; 413 | if (remainder != 0) { 414 | return FractionError.FractionIsNotInteger; 415 | } 416 | const result = @divExact(self.num, self.denom); 417 | if (result > math.maxInt(T) or result < math.minInt(T)) { 418 | return FractionError.FractionOutsideTargetRange; 419 | } 420 | const value = @as(T, @intCast(result)); 421 | return if (self.sign) 0 - value else value; 422 | }, 423 | else => @compileError("cannot convert Fraction to type " ++ @typeName(T)), 424 | } 425 | } 426 | }; 427 | 428 | test { 429 | _ = @import("fraction_test.zig"); 430 | } 431 | -------------------------------------------------------------------------------- /src/fraction_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const math = std.math; 3 | const testing = std.testing; 4 | 5 | const expect = testing.expect; 6 | const expectEqual = testing.expectEqual; 7 | const expectError = testing.expectError; 8 | 9 | const Fraction = @import("Fraction.zig").Fraction; 10 | const FractionError = @import("Fraction.zig").FractionError; 11 | 12 | test "init" { 13 | const f = try Fraction.init(1, 2, false); 14 | try expectEqual(1, f.num); 15 | try expectEqual(2, f.denom); 16 | try expectEqual(false, f.sign); 17 | 18 | const err = Fraction.init(1, 0, false); 19 | try expectError(FractionError.DenominatorCannotBeZero, err); 20 | } 21 | 22 | test "fromFloat" { 23 | const f1 = try Fraction.fromFloat(@as(f64, 2.5)); 24 | try expectEqual(5, f1.num); 25 | try expectEqual(2, f1.denom); 26 | try expectEqual(false, f1.sign); 27 | 28 | const f2 = try Fraction.fromFloat(@as(f64, -0.0)); 29 | try expectEqual(0, f2.num); 30 | try expectEqual(1, f2.denom); 31 | try expectEqual(false, f2.sign); 32 | 33 | const f3 = try Fraction.fromFloat(@as(f64, -123)); 34 | try expectEqual(123, f3.num); 35 | try expectEqual(1, f3.denom); 36 | try expectEqual(true, f3.sign); 37 | 38 | const err1 = Fraction.fromFloat(math.inf(f64)); 39 | try expectError(FractionError.CannotConvertFromFloat, err1); 40 | 41 | const err2 = Fraction.fromFloat(math.nan(f64)); 42 | try expectError(FractionError.CannotConvertFromFloat, err2); 43 | } 44 | 45 | test "toString" { 46 | const f1 = try Fraction.init(1, 2, false); 47 | const f1s = try f1.toStringAlloc(testing.allocator, .lower); 48 | defer testing.allocator.free(f1s); 49 | try expect(std.mem.eql(u8, "1/2", f1s)); 50 | 51 | const f2 = try Fraction.init(0, 10, false); 52 | const f2s = try f2.toStringAlloc(testing.allocator, .lower); 53 | defer testing.allocator.free(f2s); 54 | try expect(std.mem.eql(u8, "0/10", f2s)); 55 | 56 | const f3 = try Fraction.init(123, 456, true); 57 | const f3s = try f3.toStringAlloc(testing.allocator, .lower); 58 | defer testing.allocator.free(f3s); 59 | try expect(std.mem.eql(u8, "-123/456", f3s)); 60 | } 61 | 62 | test "format" { 63 | const f1 = try Fraction.init(123, 456, true); 64 | const f1_fmt = try std.fmt.allocPrintZ(testing.allocator, "{d}", .{f1}); 65 | defer testing.allocator.free(f1_fmt); 66 | try expect(std.mem.eql(u8, "-123/456", f1_fmt)); 67 | 68 | const f2 = try Fraction.init(std.math.maxInt(usize) - 1, std.math.maxInt(usize), true); 69 | const f2_fmt = try std.fmt.allocPrintZ(testing.allocator, "{d}", .{f2}); 70 | defer testing.allocator.free(f2_fmt); 71 | try expect(std.mem.eql(u8, "-18446744073709551614/18446744073709551615", f2_fmt)); 72 | } 73 | 74 | test "to" { 75 | const f1 = try Fraction.init(1, 2, true); 76 | const r1 = try f1.to(f64); 77 | try expectEqual(-0.5, r1); 78 | const err1 = f1.to(u64); 79 | try expectError(FractionError.FractionIsNegative, err1); 80 | 81 | const f2 = try Fraction.init(1, std.math.maxInt(u64), false); 82 | const r2 = try f2.to(f64); 83 | try expectEqual(5.421010862427522e-20, r2); 84 | const err2 = f2.to(u64); 85 | try expectError(FractionError.FractionIsNotInteger, err2); 86 | 87 | const f3 = try Fraction.init(std.math.maxInt(u64), 1, false); 88 | const r3 = try f3.to(f64); 89 | try expectEqual(18446744073709551615.0, r3); 90 | const r4 = try f3.to(u64); 91 | try expectEqual(std.math.maxInt(u64), r4); 92 | const err3 = f3.to(u8); 93 | try expectError(FractionError.FractionOutsideTargetRange, err3); 94 | 95 | const f5 = try Fraction.init(10, 2, true); 96 | const r5 = try f5.to(i8); 97 | try expectEqual(-5, r5); 98 | const r6 = try f5.to(f16); 99 | try expectEqual(-5.0, r6); 100 | } 101 | 102 | test "abs" { 103 | var f1 = try Fraction.init(1, 2, false); 104 | f1.abs(); 105 | try expectEqual(1, f1.num); 106 | try expectEqual(2, f1.denom); 107 | try expectEqual(false, f1.sign); 108 | 109 | var f2 = try Fraction.init(1, 2, true); 110 | f2.abs(); 111 | try expectEqual(1, f2.num); 112 | try expectEqual(2, f2.denom); 113 | try expectEqual(false, f2.sign); 114 | } 115 | 116 | test "toAbs" { 117 | const f1 = try Fraction.init(1, 2, false); 118 | const r1 = f1.toAbs(); 119 | try expectEqual(1, r1.num); 120 | try expectEqual(2, r1.denom); 121 | try expectEqual(false, r1.sign); 122 | 123 | const f2 = try Fraction.init(1, 2, true); 124 | const r2 = f2.toAbs(); 125 | try expectEqual(1, r2.num); 126 | try expectEqual(2, r2.denom); 127 | try expectEqual(false, r2.sign); 128 | } 129 | 130 | test "negate" { 131 | var f1 = try Fraction.init(1, 2, false); 132 | f1.negate(); 133 | try expectEqual(1, f1.num); 134 | try expectEqual(2, f1.denom); 135 | try expectEqual(true, f1.sign); 136 | 137 | var f2 = try Fraction.init(1, 2, true); 138 | f2.negate(); 139 | try expectEqual(1, f2.num); 140 | try expectEqual(2, f2.denom); 141 | try expectEqual(false, f2.sign); 142 | } 143 | 144 | test "toNegation" { 145 | const f1 = try Fraction.init(3, 6, false); 146 | const r1 = f1.toNegation(); 147 | try expectEqual(3, r1.num); 148 | try expectEqual(6, r1.denom); 149 | try expectEqual(true, r1.sign); 150 | 151 | const f2 = try Fraction.init(3, 6, true); 152 | const r2 = f2.toNegation(); 153 | try expectEqual(3, r2.num); 154 | try expectEqual(6, r2.denom); 155 | try expectEqual(false, r2.sign); 156 | } 157 | 158 | test "reciprocal" { 159 | var f1 = try Fraction.init(1, 2, false); 160 | try f1.reciprocal(); 161 | try expectEqual(2, f1.num); 162 | try expectEqual(1, f1.denom); 163 | try expectEqual(false, f1.sign); 164 | 165 | var f2 = try Fraction.init(0, 1, false); 166 | const err = f2.reciprocal(); 167 | try expectError(FractionError.DenominatorCannotBeZero, err); 168 | } 169 | 170 | test "toReciprocal" { 171 | const f1 = try Fraction.init(1, 2, false); 172 | const r = try f1.toReciprocal(); 173 | try expectEqual(2, r.num); 174 | try expectEqual(1, r.denom); 175 | try expectEqual(false, r.sign); 176 | 177 | const f2 = try Fraction.init(0, 1, false); 178 | const err = f2.toReciprocal(); 179 | try expectError(FractionError.DenominatorCannotBeZero, err); 180 | } 181 | 182 | test "eql" { 183 | const f1 = try Fraction.init(1, 2, false); 184 | const f2 = try Fraction.init(3, 6, false); 185 | try expect(try Fraction.eql(&f1, &f2)); 186 | 187 | const f3 = try Fraction.init(3, 6, true); 188 | try expect(!try Fraction.eql(&f1, &f3)); 189 | 190 | const f4 = try Fraction.init(std.math.maxInt(usize), std.math.maxInt(usize), false); 191 | const f5 = try Fraction.init(std.math.maxInt(usize) - 1, std.math.maxInt(usize) - 1, false); 192 | const err = Fraction.eql(&f4, &f5); 193 | try expectError(error.Overflow, err); 194 | } 195 | 196 | test "eqlAbs" { 197 | const f1 = try Fraction.init(1, 2, false); 198 | const f2 = try Fraction.init(2, 4, false); 199 | try expect(try Fraction.eqlAbs(&f1, &f2)); 200 | 201 | const f3 = try Fraction.init(4, 6, false); 202 | const f4 = try Fraction.init(2, 3, true); 203 | try expect(try Fraction.eqlAbs(&f3, &f4)); 204 | } 205 | 206 | test "eqlZero" { 207 | const f1 = try Fraction.init(0, 1, false); 208 | try expect(f1.eqlZero()); 209 | 210 | const f2 = try Fraction.init(1, 2, false); 211 | try expect(!f2.eqlZero()); 212 | } 213 | 214 | test "simplify" { 215 | var f1 = try Fraction.init(4, 6, false); 216 | f1.simplify(); 217 | try expectEqual(2, f1.num); 218 | try expectEqual(3, f1.denom); 219 | try expectEqual(false, f1.sign); 220 | 221 | var f2 = try Fraction.init(4, 6, true); 222 | f2.simplify(); 223 | try expectEqual(2, f2.num); 224 | try expectEqual(3, f2.denom); 225 | try expectEqual(true, f2.sign); 226 | } 227 | 228 | test "toSimplified" { 229 | const f1 = try Fraction.init(4, 6, false); 230 | const r1 = f1.toSimplified(); 231 | try expectEqual(2, r1.num); 232 | try expectEqual(3, r1.denom); 233 | try expectEqual(false, r1.sign); 234 | 235 | const f2 = try Fraction.init(4, 6, true); 236 | const r2 = f2.toSimplified(); 237 | try expectEqual(2, r2.num); 238 | try expectEqual(3, r2.denom); 239 | try expectEqual(true, r2.sign); 240 | } 241 | 242 | test "order" { 243 | const f1 = try Fraction.init(1, 2, false); 244 | const f2 = try Fraction.init(1, 3, false); 245 | try expect(try f1.order(&f2) == std.math.Order.gt); 246 | try expect(try f2.order(&f1) == std.math.Order.lt); 247 | try expect(try f1.order(&f1) == std.math.Order.eq); 248 | 249 | const f3 = try Fraction.init(1, 2, true); 250 | try expect(try f1.order(&f3) == std.math.Order.gt); 251 | try expect(try f3.order(&f1) == std.math.Order.lt); 252 | try expect(try f3.order(&f3) == std.math.Order.eq); 253 | 254 | const f4 = try Fraction.init(0, 1, false); 255 | const f5 = try Fraction.init(0, 2, true); 256 | try expect(try f4.order(&f5) == std.math.Order.eq); 257 | try expect(try f5.order(&f4) == std.math.Order.eq); 258 | try expect(try f4.order(&f4) == std.math.Order.eq); 259 | try expect(try f5.order(&f5) == std.math.Order.eq); 260 | } 261 | 262 | test "add" { 263 | // 1/2 + 1/3 = 5/6 264 | var f1 = try Fraction.init(1, 2, false); 265 | const f2 = try Fraction.init(1, 3, false); 266 | try f1.add(&f2); 267 | try expectEqual(5, f1.num); 268 | try expectEqual(6, f1.denom); 269 | try expectEqual(false, f1.sign); 270 | 271 | // 1/2 + -1/3 = 1/6 272 | var f3 = try Fraction.init(1, 2, false); 273 | const f4 = try Fraction.init(1, 3, true); 274 | try f3.add(&f4); 275 | try expectEqual(1, f3.num); 276 | try expectEqual(6, f3.denom); 277 | try expectEqual(false, f3.sign); 278 | 279 | // -1/2 + 1/3 = -1/6 280 | var f5 = try Fraction.init(1, 2, true); 281 | const f6 = try Fraction.init(1, 3, false); 282 | try f5.add(&f6); 283 | try expectEqual(1, f5.num); 284 | try expectEqual(6, f5.denom); 285 | try expectEqual(true, f5.sign); 286 | 287 | // -1/2 + -1/3 = -5/6 288 | var f7 = try Fraction.init(1, 2, true); 289 | const f8 = try Fraction.init(1, 3, true); 290 | try f7.add(&f8); 291 | try expectEqual(5, f7.num); 292 | try expectEqual(6, f7.denom); 293 | try expectEqual(true, f7.sign); 294 | 295 | // 1/3 + 1/2 = 5/6 296 | var f9 = try Fraction.init(1, 3, false); 297 | const f10 = try Fraction.init(1, 2, false); 298 | try f9.add(&f10); 299 | try expectEqual(5, f9.num); 300 | try expectEqual(6, f9.denom); 301 | try expectEqual(false, f9.sign); 302 | 303 | // 1/3 + -1/2 = -1/6 304 | var f11 = try Fraction.init(1, 3, false); 305 | const f12 = try Fraction.init(1, 2, true); 306 | try f11.add(&f12); 307 | try expectEqual(1, f11.num); 308 | try expectEqual(6, f11.denom); 309 | try expectEqual(true, f11.sign); 310 | 311 | // -1/3 + 1/2 = 1/6 312 | var f13 = try Fraction.init(1, 3, true); 313 | const f14 = try Fraction.init(1, 2, false); 314 | try f13.add(&f14); 315 | try expectEqual(1, f13.num); 316 | try expectEqual(6, f13.denom); 317 | try expectEqual(false, f13.sign); 318 | 319 | // -1/3 + -1/2 = -5/6 320 | var f15 = try Fraction.init(1, 3, true); 321 | const f17 = try Fraction.init(1, 2, true); 322 | try f15.add(&f17); 323 | try expectEqual(5, f15.num); 324 | try expectEqual(6, f15.denom); 325 | try expectEqual(true, f15.sign); 326 | 327 | var f18 = try Fraction.init(1, 2, false); 328 | try f18.add(&f18); 329 | try expectEqual(1, f18.num); 330 | try expectEqual(1, f18.denom); 331 | try expectEqual(false, f18.sign); 332 | } 333 | 334 | test "sub" { 335 | // 1/2 - 1/3 = 1/6 336 | var f1 = try Fraction.init(1, 2, false); 337 | const f2 = try Fraction.init(1, 3, false); 338 | try f1.sub(&f2); 339 | try expectEqual(1, f1.num); 340 | try expectEqual(6, f1.denom); 341 | try expectEqual(false, f1.sign); 342 | 343 | // 1/2 - -1/3 = 5/6 344 | var f3 = try Fraction.init(1, 2, false); 345 | const f4 = try Fraction.init(1, 3, true); 346 | try f3.sub(&f4); 347 | try expectEqual(5, f3.num); 348 | try expectEqual(6, f3.denom); 349 | try expectEqual(false, f3.sign); 350 | 351 | // -1/2 - 1/3 = -5/6 352 | var f5 = try Fraction.init(1, 2, true); 353 | const f6 = try Fraction.init(1, 3, false); 354 | try f5.sub(&f6); 355 | try expectEqual(5, f5.num); 356 | try expectEqual(6, f5.denom); 357 | try expectEqual(true, f5.sign); 358 | 359 | // -1/2 - -1/3 = -1/6 360 | var f7 = try Fraction.init(1, 2, true); 361 | const f8 = try Fraction.init(1, 3, true); 362 | try f7.sub(&f8); 363 | try expectEqual(1, f7.num); 364 | try expectEqual(6, f7.denom); 365 | try expectEqual(true, f7.sign); 366 | 367 | // 1/3 - 1/2 = -1/6 368 | var f9 = try Fraction.init(1, 3, false); 369 | const f10 = try Fraction.init(1, 2, false); 370 | try f9.sub(&f10); 371 | try expectEqual(1, f9.num); 372 | try expectEqual(6, f9.denom); 373 | try expectEqual(true, f9.sign); 374 | 375 | // 1/3 - -1/2 = 5/6 376 | var f11 = try Fraction.init(1, 3, false); 377 | const f12 = try Fraction.init(1, 2, true); 378 | try f11.sub(&f12); 379 | try expectEqual(5, f11.num); 380 | try expectEqual(6, f11.denom); 381 | try expectEqual(false, f11.sign); 382 | 383 | // -1/3 - 1/2 = -5/6 384 | var f13 = try Fraction.init(1, 3, true); 385 | const f14 = try Fraction.init(1, 2, false); 386 | try f13.sub(&f14); 387 | try expectEqual(5, f13.num); 388 | try expectEqual(6, f13.denom); 389 | try expectEqual(true, f13.sign); 390 | 391 | // -1/3 - -1/2 = 1/6 392 | var f15 = try Fraction.init(1, 3, true); 393 | const f17 = try Fraction.init(1, 2, true); 394 | try f15.sub(&f17); 395 | try expectEqual(1, f15.num); 396 | try expectEqual(6, f15.denom); 397 | try expectEqual(false, f15.sign); 398 | 399 | var f18 = try Fraction.init(1, 2, true); 400 | try f18.sub(&f18); 401 | try expectEqual(0, f18.num); 402 | try expectEqual(1, f18.denom); 403 | try expectEqual(false, f18.sign); 404 | } 405 | 406 | test "mul" { 407 | var f1 = try Fraction.init(1, 2, false); 408 | const f2 = try Fraction.init(3, 5, false); 409 | try f1.mul(&f2); 410 | try expectEqual(3, f1.num); 411 | try expectEqual(10, f1.denom); 412 | 413 | var f3 = try Fraction.init(1, 2, true); 414 | try f3.mul(&f3); 415 | try expectEqual(1, f3.num); 416 | try expectEqual(4, f3.denom); 417 | try expectEqual(false, f3.sign); 418 | } 419 | 420 | test "div" { 421 | var f1 = try Fraction.init(1, 2, false); 422 | const f2 = try Fraction.init(3, 5, false); 423 | try f1.div(&f2); 424 | try expectEqual(5, f1.num); 425 | try expectEqual(6, f1.denom); 426 | 427 | var f3 = try Fraction.init(1, 2, true); 428 | try f3.div(&f3); 429 | try expectEqual(1, f3.num); 430 | try expectEqual(1, f3.denom); 431 | try expectEqual(false, f3.sign); 432 | 433 | var f4 = try Fraction.init(1, 2, false); 434 | const f5 = try Fraction.init(0, 1, false); 435 | const err = f4.div(&f5); 436 | try expectError(FractionError.DivisionByZero, err); 437 | } 438 | 439 | test "floor" { 440 | var f1 = try Fraction.init(5, 2, false); 441 | f1.floor(); 442 | try expectEqual(2, f1.num); 443 | try expectEqual(1, f1.denom); 444 | try expectEqual(false, f1.sign); 445 | 446 | var f2 = try Fraction.init(1, 2, false); 447 | f2.floor(); 448 | try expectEqual(0, f2.num); 449 | try expectEqual(1, f2.denom); 450 | try expectEqual(false, f2.sign); 451 | 452 | var f3 = try Fraction.init(7, 3, true); 453 | f3.floor(); 454 | try expectEqual(2, f3.num); 455 | try expectEqual(1, f3.denom); 456 | try expectEqual(true, f3.sign); 457 | 458 | var f4 = try Fraction.init(1, 2, true); 459 | f4.floor(); 460 | try expectEqual(0, f4.num); 461 | try expectEqual(1, f4.denom); 462 | try expectEqual(true, f4.sign); 463 | } 464 | --------------------------------------------------------------------------------