Skip to main content

reifydb_routine/function/text/
format_bytes.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use reifydb_core::value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns};
5use reifydb_type::value::{constraint::bytes::MaxBytes, container::utf8::Utf8Container, r#type::Type};
6
7use crate::routine::{Function, FunctionKind, Routine, RoutineInfo, context::FunctionContext, error::RoutineError};
8
9const IEC_UNITS: [&str; 6] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
10
11pub(super) fn format_bytes_internal(bytes: i64, base: f64, units: &[&str]) -> String {
12	if bytes == 0 {
13		return "0 B".to_string();
14	}
15
16	let bytes_abs = bytes.unsigned_abs() as f64;
17	let sign = if bytes < 0 {
18		"-"
19	} else {
20		""
21	};
22
23	let mut unit_index = 0;
24	let mut value = bytes_abs;
25
26	while value >= base && unit_index < units.len() - 1 {
27		value /= base;
28		unit_index += 1;
29	}
30
31	if unit_index == 0 {
32		format!("{}{} {}", sign, bytes_abs as i64, units[0])
33	} else if value == value.floor() {
34		format!("{}{} {}", sign, value as i64, units[unit_index])
35	} else {
36		let formatted = format!("{:.2}", value);
37		let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
38		format!("{}{} {}", sign, trimmed, units[unit_index])
39	}
40}
41
42#[macro_export]
43macro_rules! process_int_column {
44	($container:expr, $row_count:expr, $base:expr, $units:expr) => {{
45		let mut result_data = Vec::with_capacity($row_count);
46
47		for i in 0..$row_count {
48			if let Some(&value) = $container.get(i) {
49				result_data.push(format_bytes_internal(value as i64, $base, $units));
50			} else {
51				result_data.push(String::new());
52			}
53		}
54
55		ColumnBuffer::Utf8 {
56			container: Utf8Container::new(result_data),
57			max_bytes: MaxBytes::MAX,
58		}
59	}};
60}
61
62#[macro_export]
63macro_rules! process_float_column {
64	($container:expr, $row_count:expr, $base:expr, $units:expr) => {{
65		let mut result_data = Vec::with_capacity($row_count);
66
67		for i in 0..$row_count {
68			if let Some(&value) = $container.get(i) {
69				result_data.push(format_bytes_internal(value as i64, $base, $units));
70			} else {
71				result_data.push(String::new());
72			}
73		}
74
75		ColumnBuffer::Utf8 {
76			container: Utf8Container::new(result_data),
77			max_bytes: MaxBytes::MAX,
78		}
79	}};
80}
81
82#[macro_export]
83macro_rules! process_decimal_column {
84	($container:expr, $row_count:expr, $base:expr, $units:expr) => {{
85		let mut result_data = Vec::with_capacity($row_count);
86
87		for i in 0..$row_count {
88			if let Some(value) = $container.get(i) {
89				let s = value.to_string();
90				let int_part = s.split('.').next().unwrap_or("0");
91				let bytes = int_part.parse::<i64>().unwrap_or(0);
92				result_data.push(format_bytes_internal(bytes, $base, $units));
93			} else {
94				result_data.push(String::new());
95			}
96		}
97
98		ColumnBuffer::Utf8 {
99			container: Utf8Container::new(result_data),
100			max_bytes: MaxBytes::MAX,
101		}
102	}};
103}
104
105pub struct FormatBytes {
106	info: RoutineInfo,
107}
108
109impl Default for FormatBytes {
110	fn default() -> Self {
111		Self::new()
112	}
113}
114
115impl FormatBytes {
116	pub fn new() -> Self {
117		Self {
118			info: RoutineInfo::new("text::format_bytes"),
119		}
120	}
121}
122
123impl<'a> Routine<FunctionContext<'a>> for FormatBytes {
124	fn info(&self) -> &RoutineInfo {
125		&self.info
126	}
127
128	fn return_type(&self, _input_types: &[Type]) -> Type {
129		Type::Utf8
130	}
131
132	fn execute(&self, ctx: &mut FunctionContext<'a>, args: &Columns) -> Result<Columns, RoutineError> {
133		if args.len() != 1 {
134			return Err(RoutineError::FunctionArityMismatch {
135				function: ctx.fragment.clone(),
136				expected: 1,
137				actual: args.len(),
138			});
139		}
140
141		let column = &args[0];
142		let (data, bitvec) = column.unwrap_option();
143		let row_count = data.len();
144
145		let result_data = match data {
146			ColumnBuffer::Int1(container) => process_int_column!(container, row_count, 1024.0, &IEC_UNITS),
147			ColumnBuffer::Int2(container) => process_int_column!(container, row_count, 1024.0, &IEC_UNITS),
148			ColumnBuffer::Int4(container) => process_int_column!(container, row_count, 1024.0, &IEC_UNITS),
149			ColumnBuffer::Int8(container) => process_int_column!(container, row_count, 1024.0, &IEC_UNITS),
150			ColumnBuffer::Uint1(container) => process_int_column!(container, row_count, 1024.0, &IEC_UNITS),
151			ColumnBuffer::Uint2(container) => process_int_column!(container, row_count, 1024.0, &IEC_UNITS),
152			ColumnBuffer::Uint4(container) => process_int_column!(container, row_count, 1024.0, &IEC_UNITS),
153			ColumnBuffer::Uint8(container) => process_int_column!(container, row_count, 1024.0, &IEC_UNITS),
154			ColumnBuffer::Float4(container) => {
155				process_float_column!(container, row_count, 1024.0, &IEC_UNITS)
156			}
157			ColumnBuffer::Float8(container) => {
158				process_float_column!(container, row_count, 1024.0, &IEC_UNITS)
159			}
160			ColumnBuffer::Decimal {
161				container,
162				..
163			} => {
164				process_decimal_column!(container, row_count, 1024.0, &IEC_UNITS)
165			}
166			other => {
167				return Err(RoutineError::FunctionInvalidArgumentType {
168					function: ctx.fragment.clone(),
169					argument_index: 0,
170					expected: vec![
171						Type::Int1,
172						Type::Int2,
173						Type::Int4,
174						Type::Int8,
175						Type::Uint1,
176						Type::Uint2,
177						Type::Uint4,
178						Type::Uint8,
179						Type::Float4,
180						Type::Float8,
181						Type::Decimal,
182					],
183					actual: other.get_type(),
184				});
185			}
186		};
187
188		let final_data = match bitvec {
189			Some(bv) => ColumnBuffer::Option {
190				inner: Box::new(result_data),
191				bitvec: bv.clone(),
192			},
193			None => result_data,
194		};
195		Ok(Columns::new(vec![ColumnWithName::new(ctx.fragment.clone(), final_data)]))
196	}
197}
198
199impl Function for FormatBytes {
200	fn kinds(&self) -> &[FunctionKind] {
201		&[FunctionKind::Scalar]
202	}
203}
204
205pub(super) use process_decimal_column;
206pub(super) use process_float_column;
207pub(super) use process_int_column;