compress_json_rs/encode.rs
1//! Encoding and decoding functions for compressed values.
2//!
3//! This module provides internal functions for encoding JSON values into
4//! their compressed string representations and decoding them back.
5//!
6//! # Encoding Format
7//!
8//! | Type | Prefix | Example |
9//! |------|--------|---------|
10//! | Boolean true | `b\|T` | `"b\|T"` |
11//! | Boolean false | `b\|F` | `"b\|F"` |
12//! | Number | `n\|` | `"n\|42.5"` |
13//! | Infinity | `N\|+` | `"N\|+"` (when preserved) |
14//! | -Infinity | `N\|-` | `"N\|-"` (when preserved) |
15//! | NaN | `N\|0` | `"N\|0"` (when preserved) |
16//! | Escaped string | `s\|` | `"s\|n\|foo"` |
17//! | Plain string | _(none)_ | `"hello"` |
18//!
19//! # String Escaping
20//!
21//! Strings that start with reserved prefixes (`b|`, `n|`, `N|`, `o|`, `a|`, `s|`)
22//! are escaped with `s|` to prevent ambiguity during decoding.
23//!
24//! # Special Values (v3.4.0+)
25//!
26//! Special floating-point values have dedicated encodings when enabled via config:
27//! - `Infinity` → `N|+` (when `preserve_infinite` is true)
28//! - `-Infinity` → `N|-` (when `preserve_infinite` is true)
29//! - `NaN` → `N|0` (when `preserve_nan` is true)
30//!
31//! When preservation is disabled (default), special values become `null` like `JSON.stringify`.
32//! This ensures compatibility with JavaScript and Python implementations v3.4.0+.
33
34use crate::number::s_to_int;
35
36/// Encode a regular number to compressed string with 'n|' prefix.
37///
38/// This function is for regular (finite) numbers only. Special values
39/// (Infinity, -Infinity, NaN) are handled separately in `memory.rs`
40/// based on configuration settings.
41///
42/// # Arguments
43///
44/// * `num` - The f64 number to encode (should be finite)
45///
46/// # Returns
47///
48/// String in format `"n|<number>"`
49///
50/// # Example
51///
52/// ```ignore
53/// assert_eq!(encode_num(42.5), "n|42.5");
54/// assert_eq!(encode_num(-3.14), "n|-3.14");
55/// assert_eq!(encode_num(0.0), "n|0");
56/// ```
57///
58/// # Note
59///
60/// For special values (Infinity, NaN), the handling depends on config:
61/// - `preserve_nan`/`preserve_infinite`: encoded as `N|0`, `N|+`, `N|-`
62/// - Otherwise: converted to null (empty string)
63pub fn encode_num(num: f64) -> String {
64 format!("n|{num}")
65}
66
67/// Check if an encoded string represents a special value (Infinity/NaN).
68///
69/// # Arguments
70///
71/// * `s` - The encoded string to check
72///
73/// # Returns
74///
75/// `true` if the string starts with `N|` (special value prefix)
76pub fn is_special_value(s: &str) -> bool {
77 s.starts_with("N|")
78}
79
80/// Decode a special value string to f64.
81///
82/// # Arguments
83///
84/// * `s` - String starting with "N|" prefix
85///
86/// # Returns
87///
88/// - `f64::INFINITY` for "N|+"
89/// - `f64::NEG_INFINITY` for "N|-"
90/// - `f64::NAN` for "N|0"
91///
92/// # Panics
93///
94/// Panics if the string is not a valid special value encoding.
95pub fn decode_special(s: &str) -> f64 {
96 match s {
97 "N|+" => f64::INFINITY,
98 "N|-" => f64::NEG_INFINITY,
99 "N|0" => f64::NAN,
100 _ => panic!("Invalid special value encoding: {s}"),
101 }
102}
103
104/// Decode a compressed number string to f64.
105///
106/// # Arguments
107///
108/// * `s` - String starting with "n|" prefix
109///
110/// # Returns
111///
112/// The decoded f64 value
113///
114/// # Panics
115///
116/// Panics if the string after the prefix is not a valid number.
117pub fn decode_num(s: &str) -> f64 {
118 let s2 = s.strip_prefix("n|").unwrap_or(s);
119 s2.parse::<f64>().expect("invalid number")
120}
121
122/// Decode a key string (base-62) to an index.
123///
124/// Converts a base-62 encoded key back to its numeric index
125/// in the values array.
126///
127/// # Arguments
128///
129/// * `key` - Base-62 encoded key string
130///
131/// # Returns
132///
133/// The numeric index as usize
134pub fn decode_key(key: &str) -> usize {
135 s_to_int(key)
136}
137
138/// Encode a boolean to compressed string with 'b|' prefix.
139///
140/// # Arguments
141///
142/// * `b` - Boolean value to encode
143///
144/// # Returns
145///
146/// `"b|T"` for true, `"b|F"` for false
147pub fn encode_bool(b: bool) -> String {
148 if b {
149 "b|T".to_string()
150 } else {
151 "b|F".to_string()
152 }
153}
154
155/// Decode a compressed boolean string to bool.
156///
157/// # Arguments
158///
159/// * `s` - String "b|T" or "b|F"
160///
161/// # Returns
162///
163/// `true` for "b|T", `false` for "b|F" or empty string
164pub fn decode_bool(s: &str) -> bool {
165 match s {
166 "b|T" => true,
167 "b|F" => false,
168 _ => !s.is_empty(),
169 }
170}
171
172/// Encode a string, escaping reserved prefixes with 's|' if needed.
173///
174/// If the string starts with a reserved prefix (`b|`, `o|`, `n|`, `N|`, `a|`, `s|`),
175/// it's escaped by prepending `s|` to prevent decoding ambiguity.
176///
177/// # Arguments
178///
179/// * `s` - The string to encode
180///
181/// # Returns
182///
183/// The original string, or escaped with `s|` prefix if needed
184///
185/// # Example
186///
187/// ```ignore
188/// assert_eq!(encode_str("hello"), "hello");
189/// assert_eq!(encode_str("n|123"), "s|n|123"); // Escaped
190/// assert_eq!(encode_str("N|+"), "s|N|+"); // Escaped (v3.2.0)
191/// ```
192pub fn encode_str(s: &str) -> String {
193 // Check for reserved prefixes using starts_with (UTF-8 safe)
194 // Note: N| added in v3.2.0 for special values
195 if s.starts_with("b|")
196 || s.starts_with("o|")
197 || s.starts_with("n|")
198 || s.starts_with("N|")
199 || s.starts_with("a|")
200 || s.starts_with("s|")
201 {
202 return format!("s|{s}");
203 }
204 s.to_string()
205}
206
207/// Decode a compressed string, unescaping 's|' prefix if present.
208///
209/// # Arguments
210///
211/// * `s` - The encoded string
212///
213/// # Returns
214///
215/// The original string with `s|` prefix removed if present
216pub fn decode_str(s: &str) -> String {
217 // Use strip_prefix for safe UTF-8 handling
218 if let Some(stripped) = s.strip_prefix("s|") {
219 stripped.to_string()
220 } else {
221 s.to_string()
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_decode_special_values() {
231 assert_eq!(decode_special("N|+"), f64::INFINITY);
232 assert_eq!(decode_special("N|-"), f64::NEG_INFINITY);
233 assert!(decode_special("N|0").is_nan());
234 }
235
236 #[test]
237 fn test_encode_regular_numbers() {
238 assert_eq!(encode_num(42.0), "n|42");
239 assert_eq!(encode_num(-3.14), "n|-3.14");
240 assert_eq!(encode_num(0.0), "n|0");
241 }
242
243 #[test]
244 fn test_escape_special_prefix() {
245 assert_eq!(encode_str("N|+"), "s|N|+");
246 assert_eq!(encode_str("N|-"), "s|N|-");
247 assert_eq!(encode_str("N|0"), "s|N|0");
248 }
249}