reifydb_routine/function/text/
format_bytes.rs1use 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;