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}