diff --git a/Makefile b/Makefile index 46fd88e..f181e20 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ CC=cc RUSTC=rustc .PHONY: all -all: dj false fop intcmp rpn scrut str strcmp true +all: dj false fop hru intcmp rpn scrut str strcmp true build: # keep build/include until bindgen(1) has stdin support @@ -74,6 +74,13 @@ build/bin/fop: src/fop.rs build build/o/libgetopt.rlib build/o/libsysexits.rlib --extern sysexits=build/o/libsysexits.rlib \ -o $@ src/fop.rs +.PHONY: hru +hru: build/bin/hru +build/bin/hru: src/hru.rs build build/o/libgetopt.rlib build/o/libsysexits.rlib + $(RUSTC) $(RUSTFLAGS) --extern getopt=build/o/libgetopt.rlib \ + --extern sysexits=build/o/libsysexits.rlib \ + -o $@ src/hru.rs + .PHONY: intcmp intcmp: build/bin/intcmp build/bin/intcmp: src/intcmp.c build diff --git a/docs/hru.1 b/docs/hru.1 new file mode 100644 index 0000000..c75cb73 --- /dev/null +++ b/docs/hru.1 @@ -0,0 +1,57 @@ +.\" Copyright (c) 2024 Emma Tebibyte +.\" +.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, +.\" visit . + +.TH rpn 1 + +.SH NAME + +hru \(en human readable units + +.SH SYNOPSIS + +hru + +.SH DESCRIPTION + +Hru reads byte counts in the form of whole numbers from the standard input and +writes to the standard output the same number converted one of the units of data +defined by the International System of Units. + +The program will convert the byte count to the highest unit possible where the +value is greater than one. + +.SH DIAGNOSTICS + +If encountering non-integer characters in the standard input, hru will exit with +the appropriate error code as defined by sysexits.h(3) and print an error +message. + +.SH RATIONALE + +The GNU project’s ls(1) implementation contains a human-readable option (-h) +that, when specified, makes the tool print size information in a format more +immediately readable. This functionality is useful not only in the context of +ls(1) so the decision was made to split it into a new tool. The original +functionality in GNU’s ls(1) can be emulated with fop(1) combined with this +program. + +.SH STANDARDS + +Hru follows the standard unit prefixes as specified by the Bureau International +des Poids et Mesures (BIPM) in the ninth edition of The International System of +Units (SI). + +.SH AUTHOR + +Written by Emma Tebibyte . + +.SH COPYRIGHT + +Copyright (c) 2024 Emma Tebibyte. License AGPLv3+: GNU AGPL version 3 or later +. + +.SH SEE ALSO + +GNU ls(1), The International System of Units (SI) 9th Edition diff --git a/src/hru.rs b/src/hru.rs new file mode 100644 index 0000000..0e0b25d --- /dev/null +++ b/src/hru.rs @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023–2024 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +use std::{ + cmp::Ordering, + env::args, + io::{ stdin, stdout, Write }, + process::{ ExitCode, exit }, +}; + +extern crate sysexits; + +use sysexits::{ EX_DATAERR, EX_IOERR, EX_SOFTWARE }; + +const LIST: [(u32, &str); 10] = [ + (3, "k"), + (6, "M"), + (9, "G"), + (12, "T"), + (15, "P"), + (18, "E"), + (21, "Z"), + (24, "Y"), + (27, "R"), + (30, "Q") +]; + +fn convert(input: u128) -> Result<(f64, (u32, &'static str)), String> { + + let mut out = (input as f64, (0_u32, "")); + if input < 1000 { return Ok(out); } + + for (n, p) in LIST { + let c = match 10_u128.checked_pow(n) { + Some(c) => c, + None => { + return Err(format!("10^{}: Integer overflow.", n.to_string())); + }, + }; + + match c.cmp(&input) { + Ordering::Less => { + out = (input as f64 / c as f64, (n, p)); + }, + Ordering::Equal => { + return Ok((input as f64 / c as f64, (n, p))); + }, + Ordering::Greater => {}, + }; + } + + Ok(out) +} + +fn main() -> ExitCode { + let argv = args().collect::>(); + let mut buf = String::new(); + while let Ok(_) = stdin().read_line(&mut buf) { + if buf.is_empty() { return ExitCode::SUCCESS; } + + let n: u128 = match buf.trim().parse() { + Ok(f) => { + buf.clear(); + f + }, + Err(err) => { + eprintln!("{}: {}.", argv[0], err); + return ExitCode::from(EX_DATAERR as u8); + }, + }; + + let (number, prefix) = match convert(n) { + Ok(x) => x, + Err(err) => { + eprintln!("{}: {}.", argv[0], err); + return ExitCode::from(EX_SOFTWARE as u8); + }, + }; + + let si_prefix = format!("{}B", prefix.1); + + let out = ((number * 10.0).round() / 10.0).to_string(); + + stdout().write_all(format!("{} {}\n", out, si_prefix).as_bytes()) + .unwrap_or_else(|e| { + eprintln!("{}: {}.", argv[0], e); + exit(EX_IOERR); + }); + } + + ExitCode::SUCCESS +}