Skip to main content

i_ching/
cli.rs

1use crate::core::data::IChingData;
2use crate::core::{Diviner, Reading};
3use anyhow::Result;
4use clap::{Parser, ValueEnum};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct JsonHexagram {
9    pub number: u8,
10    pub name: String,
11    pub chinese: String,
12    pub pinyin: String,
13    pub unicode: String,
14    pub description: String,
15    pub judgment: JsonJudgment,
16    pub image: JsonImage,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct JsonJudgment {
21    pub text: String,
22    pub commentary: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct JsonImage {
27    pub text: String,
28    pub commentary: String,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct JsonLineInterpretation {
33    pub position: u8,
34    pub text: String,
35    pub comments: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct JsonReading {
40    pub question: Option<String>,
41    pub lines: [u8; 6],
42    pub primary_hexagram: JsonHexagram,
43    pub changing_lines: Vec<JsonLineInterpretation>,
44    pub transformed_hexagram: Option<JsonHexagram>,
45    pub upper_trigram: [String; 3],
46    pub lower_trigram: [String; 3],
47}
48
49#[derive(Parser)]
50#[command(name = "i-ching")]
51#[command(about = "I Ching divination readings")]
52#[command(version = env!("CARGO_PKG_VERSION"))]
53pub struct Cli {
54    /// Output format
55    #[arg(short, long, default_value = "full")]
56    pub format: Format,
57
58    /// Input for reading: hexagram number (1-64), Unicode character (䷀ to ䷿), line numbers (6,7,8,9) comma separated, or changing format (32→34 or ䷟→䷡)
59    #[arg(short, long)]
60    pub input: Option<String>,
61}
62
63#[derive(ValueEnum, Clone)]
64pub enum Format {
65    Brief,
66    Full,
67    Json,
68    Numbers,
69    Motd,
70}
71
72pub fn run_cli() -> Result<()> {
73    let cli = Cli::parse();
74    let mut diviner = Diviner::new();
75
76    let reading = if let Some(input) = cli.input {
77        parse_input_and_create_reading(&mut diviner, &input)?
78    } else {
79        // No input provided, cast randomly using coins method
80        diviner.cast_reading(None)
81    };
82
83    match cli.format {
84        Format::Json => {
85            let json_reading = create_json_reading(&reading)?;
86            println!("{}", serde_json::to_string_pretty(&json_reading)?);
87        }
88        Format::Numbers => {
89            println!("{:?}", reading.traditional_numbers());
90        }
91        Format::Brief => {
92            println!("{}", format_brief(&reading)?);
93        }
94        Format::Full => {
95            println!("{}", format_full(&reading)?);
96        }
97        Format::Motd => {
98            println!("{}", format_motd(&reading)?);
99        }
100    }
101
102    Ok(())
103}
104
105/// Parse input string and create a reading based on the input type
106fn parse_input_and_create_reading(diviner: &mut Diviner, input: &str) -> Result<Reading> {
107    let input = input.trim();
108
109    // Try to parse as changing hexagram format (Unicode or numbers)
110    // Supports: ䷟→䷡, ䷟->䷡, 32->34, 32→34
111    if let Some(reading) = try_parse_changing_hexagram(input)? {
112        return Ok(reading);
113    }
114
115    // Try to parse as hexagram number (1-64)
116    if let Ok(hexagram_number) = input.parse::<u8>() {
117        if hexagram_number >= 1 && hexagram_number <= 64 {
118            return create_reading_from_hexagram_number(hexagram_number);
119        }
120    }
121
122    // Try to parse as Unicode hexagram character
123    if input.chars().count() == 1 {
124        let unicode_char = input.chars().next().unwrap();
125        if let Some(hexagram_number) = unicode_to_hexagram_number(unicode_char)? {
126            return create_reading_from_hexagram_number(hexagram_number);
127        }
128    }
129
130    // Try to parse as comma-separated line numbers (6,7,8,9)
131    if input.contains(',') {
132        let line_numbers: Result<Vec<u8>, _> =
133            input.split(',').map(|s| s.trim().parse::<u8>()).collect();
134
135        if let Ok(numbers) = line_numbers {
136            if numbers.len() == 6 && numbers.iter().all(|&n| [6, 7, 8, 9].contains(&n)) {
137                let lines_array: [u8; 6] = numbers
138                    .try_into()
139                    .map_err(|_| anyhow::anyhow!("Failed to convert line numbers to array"))?;
140                return diviner.cast_reading_from_numbers(lines_array, None);
141            }
142        }
143    }
144
145    Err(anyhow::anyhow!(
146        "Invalid input: '{}'. Expected hexagram number (1-64), Unicode character (䷀-䷿), changing format (32→34 or ䷟→䷡), or comma-separated line numbers (6,7,8,9)",
147        input
148    ))
149}
150
151/// Try to parse changing hexagram format like 32→34, 32->34, ䷟→䷡, ䷟->䷡
152fn try_parse_changing_hexagram(input: &str) -> Result<Option<Reading>> {
153    // Look for arrow indicators (both Unicode and ASCII)
154    let separators = ["→", "->"];
155
156    for separator in &separators {
157        if let Some(arrow_pos) = input.find(separator) {
158            let (from_part, to_part) = input.split_at(arrow_pos);
159            let to_part = &to_part[separator.len()..];
160            let from_part = from_part.trim();
161            let to_part = to_part.trim();
162
163            // Try to parse both parts as hexagram numbers
164            if let (Ok(from_num), Ok(to_num)) = (from_part.parse::<u8>(), to_part.parse::<u8>()) {
165                if from_num >= 1 && from_num <= 64 && to_num >= 1 && to_num <= 64 {
166                    return Ok(Some(create_changing_reading_from_numbers(
167                        from_num, to_num,
168                    )?));
169                }
170            }
171
172            // Try to parse both parts as Unicode characters
173            if from_part.chars().count() == 1 && to_part.chars().count() == 1 {
174                let from_char = from_part.chars().next().unwrap();
175                let to_char = to_part.chars().next().unwrap();
176
177                if let (Some(from_num), Some(to_num)) = (
178                    unicode_to_hexagram_number(from_char)?,
179                    unicode_to_hexagram_number(to_char)?,
180                ) {
181                    return Ok(Some(create_changing_reading_from_numbers(
182                        from_num, to_num,
183                    )?));
184                }
185            }
186        }
187    }
188
189    Ok(None)
190}
191
192/// Convert Unicode hexagram character to hexagram number
193fn unicode_to_hexagram_number(unicode_char: char) -> Result<Option<u8>> {
194    let data =
195        IChingData::load().map_err(|e| anyhow::anyhow!("Failed to load I Ching data: {}", e))?;
196
197    // Search through all hexagrams to find matching Unicode character
198    for i in 1..=64 {
199        if let Some(hexagram) = data.get_hexagram(i) {
200            if hexagram.unicode.chars().next() == Some(unicode_char) {
201                return Ok(Some(i));
202            }
203        }
204    }
205
206    Ok(None)
207}
208
209/// Create a reading from a hexagram number by generating all young lines (no changing lines)
210fn create_reading_from_hexagram_number(hexagram_number: u8) -> Result<Reading> {
211    // Convert hexagram number back to binary representation
212    // Hexagram numbers are 1-indexed, so subtract 1 to get 0-63 range
213    let binary_value = hexagram_number - 1;
214
215    let mut lines = [crate::core::reading::Line::new(
216        crate::core::reading::Age::Young,
217        crate::core::reading::Polarity::Yin,
218    ); 6];
219
220    // Convert binary representation to lines (bottom to top)
221    for i in 0..6 {
222        let bit = (binary_value >> i) & 1;
223        lines[i] = crate::core::reading::Line::new(
224            crate::core::reading::Age::Young,
225            if bit == 1 {
226                crate::core::reading::Polarity::Yang
227            } else {
228                crate::core::reading::Polarity::Yin
229            },
230        );
231    }
232
233    Ok(Reading::new(lines, None))
234}
235
236/// Create a reading that changes from one hexagram to another
237fn create_changing_reading_from_numbers(from_hexagram: u8, to_hexagram: u8) -> Result<Reading> {
238    // Convert hexagram numbers to binary representations
239    let from_binary = from_hexagram - 1;
240    let to_binary = to_hexagram - 1;
241
242    let mut lines = [crate::core::reading::Line::new(
243        crate::core::reading::Age::Young,
244        crate::core::reading::Polarity::Yin,
245    ); 6];
246
247    // Create lines that will transform from_hexagram into to_hexagram
248    for i in 0..6 {
249        let from_bit = (from_binary >> i) & 1;
250        let to_bit = (to_binary >> i) & 1;
251
252        let from_polarity = if from_bit == 1 {
253            crate::core::reading::Polarity::Yang
254        } else {
255            crate::core::reading::Polarity::Yin
256        };
257
258        let to_polarity = if to_bit == 1 {
259            crate::core::reading::Polarity::Yang
260        } else {
261            crate::core::reading::Polarity::Yin
262        };
263
264        // If the polarity changes, make it an old line (changing)
265        // If it stays the same, make it a young line (stable)
266        if from_polarity != to_polarity {
267            lines[i] =
268                crate::core::reading::Line::new(crate::core::reading::Age::Old, from_polarity);
269        } else {
270            lines[i] =
271                crate::core::reading::Line::new(crate::core::reading::Age::Young, from_polarity);
272        }
273    }
274
275    let reading = Reading::new(lines, None);
276
277    // Verify that our reading actually transforms correctly
278    if reading.primary_hexagram() != from_hexagram {
279        return Err(anyhow::anyhow!(
280            "Internal error: created reading has hexagram {} but expected {}",
281            reading.primary_hexagram(),
282            from_hexagram
283        ));
284    }
285
286    if let Some(transformed) = reading.transformed_hexagram() {
287        if transformed.primary_hexagram() != to_hexagram {
288            return Err(anyhow::anyhow!(
289                "Internal error: transformed reading has hexagram {} but expected {}",
290                transformed.primary_hexagram(),
291                to_hexagram
292            ));
293        }
294    } else if from_hexagram != to_hexagram {
295        return Err(anyhow::anyhow!(
296            "Internal error: reading should have changing lines but doesn't"
297        ));
298    }
299
300    Ok(reading)
301}
302
303/// Create a JSON representation of a reading with full meanings
304fn create_json_reading(reading: &Reading) -> Result<JsonReading> {
305    let data =
306        IChingData::load().map_err(|e| anyhow::anyhow!("Failed to load I Ching data: {}", e))?;
307
308    let hexagram_number = reading.primary_hexagram();
309    let hexagram = data
310        .get_hexagram(hexagram_number)
311        .ok_or_else(|| anyhow::anyhow!("Hexagram {} not found", hexagram_number))?;
312
313    let primary_hexagram = JsonHexagram {
314        number: hexagram.number,
315        name: hexagram.name.clone(),
316        chinese: hexagram.chinese.clone(),
317        pinyin: hexagram.pinyin.clone(),
318        unicode: hexagram.unicode.clone(),
319        description: hexagram.description.clone(),
320        judgment: JsonJudgment {
321            text: hexagram.judgment.text.clone(),
322            commentary: hexagram.judgment.commentary.clone(),
323        },
324        image: JsonImage {
325            text: hexagram.image.text.clone(),
326            commentary: hexagram.image.commentary.clone(),
327        },
328    };
329
330    let changing_lines: Vec<JsonLineInterpretation> = reading
331        .changing_line_positions()
332        .into_iter()
333        .filter_map(|line_pos| {
334            data.get_line_interpretation(hexagram_number, line_pos)
335                .map(|interp| JsonLineInterpretation {
336                    position: line_pos,
337                    text: interp.text.clone(),
338                    comments: interp.comments.clone(),
339                })
340        })
341        .collect();
342
343    let transformed_hexagram = if let Some(transformed) = reading.transformed_hexagram() {
344        let transformed_number = transformed.primary_hexagram();
345        data.get_hexagram(transformed_number)
346            .map(|hex| JsonHexagram {
347                number: hex.number,
348                name: hex.name.clone(),
349                chinese: hex.chinese.clone(),
350                pinyin: hex.pinyin.clone(),
351                unicode: hex.unicode.clone(),
352                description: hex.description.clone(),
353                judgment: JsonJudgment {
354                    text: hex.judgment.text.clone(),
355                    commentary: hex.judgment.commentary.clone(),
356                },
357                image: JsonImage {
358                    text: hex.image.text.clone(),
359                    commentary: hex.image.commentary.clone(),
360                },
361            })
362    } else {
363        None
364    };
365
366    let polarity_to_string = |polarity| match polarity {
367        crate::core::reading::Polarity::Yang => "Yang".to_string(),
368        crate::core::reading::Polarity::Yin => "Yin".to_string(),
369    };
370
371    let upper_trigram = reading.upper_trigram().map(polarity_to_string);
372    let lower_trigram = reading.lower_trigram().map(polarity_to_string);
373
374    Ok(JsonReading {
375        question: reading.question.clone(),
376        lines: reading.traditional_numbers(),
377        primary_hexagram,
378        changing_lines,
379        transformed_hexagram,
380        upper_trigram,
381        lower_trigram,
382    })
383}
384
385fn format_brief(reading: &Reading) -> Result<String> {
386    let data =
387        IChingData::load().map_err(|e| anyhow::anyhow!("Failed to load I Ching data: {}", e))?;
388    let mut result = String::new();
389
390    if let Some(ref question) = reading.question {
391        result.push_str(&format!("Q: {}\n", question));
392    }
393
394    let hexagram_number = reading.primary_hexagram();
395    if let Some(hexagram) = data.get_hexagram(hexagram_number) {
396        result.push_str(&format!(
397            "{} {} {}",
398            hexagram.unicode, hexagram_number, hexagram.name
399        ));
400
401        if reading.has_changing_lines() {
402            if let Some(transformed) = reading.transformed_hexagram() {
403                let transformed_number = transformed.primary_hexagram();
404                if let Some(transformed_hex) = data.get_hexagram(transformed_number) {
405                    result.push_str(&format!(
406                        " → {} {} {}",
407                        transformed_hex.unicode, transformed_number, transformed_hex.name
408                    ));
409                } else {
410                    result.push_str(&format!(" → {} {}", transformed_number, "Unknown"));
411                }
412            }
413            result.push_str(&format!(
414                " (lines: {:?})",
415                reading.changing_line_positions()
416            ));
417        }
418    } else {
419        result.push_str(&format!("Hexagram {} (Unknown)", hexagram_number));
420    }
421
422    Ok(result)
423}
424
425fn format_full(reading: &Reading) -> Result<String> {
426    let data =
427        IChingData::load().map_err(|e| anyhow::anyhow!("Failed to load I Ching data: {}", e))?;
428    let mut result = reading.display();
429
430    // Add traditional numbers for reference
431    result.push_str(&format!(
432        "\nTraditional numbers: {:?}\n",
433        reading.traditional_numbers()
434    ));
435
436    // Add trigram information
437    result.push_str(&format!("Upper trigram: {:?}\n", reading.upper_trigram()));
438    result.push_str(&format!("Lower trigram: {:?}\n", reading.lower_trigram()));
439
440    // Add hexagram meanings
441    let hexagram_number = reading.primary_hexagram();
442    if let Some(hexagram) = data.get_hexagram(hexagram_number) {
443        result.push_str(&format!(
444            "\n=== {} {} ===\n",
445            hexagram.unicode, hexagram.name
446        ));
447        result.push_str(&format!(
448            "Chinese: {} ({})\n",
449            hexagram.chinese, hexagram.pinyin
450        ));
451        result.push_str(&format!("Description: {}\n", hexagram.description));
452
453        result.push_str(&format!("\nJudgment: {}\n", hexagram.judgment.text));
454        result.push_str(&format!("Commentary: {}\n", hexagram.judgment.commentary));
455
456        result.push_str(&format!("\nImage: {}\n", hexagram.image.text));
457        result.push_str(&format!(
458            "Image Commentary: {}\n",
459            hexagram.image.commentary
460        ));
461
462        // Add changing line interpretations
463        if reading.has_changing_lines() {
464            result.push_str("\n=== Changing Lines ===\n");
465            for &line_pos in &reading.changing_line_positions() {
466                if let Some(line_interp) = data.get_line_interpretation(hexagram_number, line_pos) {
467                    result.push_str(&format!("Line {}: {}\n", line_pos, line_interp.text));
468                    result.push_str(&format!("Comments: {}\n\n", line_interp.comments));
469                }
470            }
471
472            // Add transformed hexagram meaning
473            if let Some(transformed) = reading.transformed_hexagram() {
474                let transformed_number = transformed.primary_hexagram();
475                if let Some(transformed_hex) = data.get_hexagram(transformed_number) {
476                    result.push_str(&format!(
477                        "\n=== Transforms to {} {} ===\n",
478                        transformed_hex.unicode, transformed_hex.name
479                    ));
480                    result.push_str(&format!(
481                        "Chinese: {} ({})\n",
482                        transformed_hex.chinese, transformed_hex.pinyin
483                    ));
484                    result.push_str(&format!("Description: {}\n", transformed_hex.description));
485                    result.push_str(&format!("Judgment: {}\n", transformed_hex.judgment.text));
486                }
487            }
488        }
489    }
490
491    Ok(result)
492}
493
494fn format_motd(reading: &Reading) -> Result<String> {
495    let data =
496        IChingData::load().map_err(|e| anyhow::anyhow!("Failed to load I Ching data: {}", e))?;
497    let hexagram_number = reading.primary_hexagram();
498
499    if let Some(hexagram) = data.get_hexagram(hexagram_number) {
500        if reading.has_changing_lines() {
501            if let Some(transformed) = reading.transformed_hexagram() {
502                let transformed_number = transformed.primary_hexagram();
503                if let Some(transformed_hex) = data.get_hexagram(transformed_number) {
504                    Ok(format!(
505                        "{}→{} {} {} CHANGING INTO {} {}",
506                        hexagram.unicode,
507                        transformed_hex.unicode,
508                        hexagram_number,
509                        hexagram.name.to_uppercase(),
510                        transformed_number,
511                        transformed_hex.name.to_uppercase()
512                    ))
513                } else {
514                    Ok(format!(
515                        "{}→䷜ {} {} CHANGING INTO {} UNKNOWN",
516                        hexagram.unicode,
517                        hexagram_number,
518                        hexagram.name.to_uppercase(),
519                        transformed_number
520                    ))
521                }
522            } else {
523                // This shouldn't happen if has_changing_lines() is true, but just in case
524                Ok(format!(
525                    "{} {} {}",
526                    hexagram.unicode,
527                    hexagram_number,
528                    hexagram.name.to_uppercase()
529                ))
530            }
531        } else {
532            Ok(format!(
533                "{} {} {}",
534                hexagram.unicode,
535                hexagram_number,
536                hexagram.name.to_uppercase()
537            ))
538        }
539    } else {
540        Ok(format!("䷜ {} UNKNOWN", hexagram_number))
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn test_format_brief() {
550        let diviner = Diviner::new();
551        let reading = diviner
552            .cast_reading_from_numbers([7, 8, 9, 6, 7, 8], Some("Test question".to_string()))
553            .unwrap();
554
555        let brief = format_brief(&reading).unwrap();
556        println!("Brief output: '{}'", brief);
557        assert!(brief.contains("Q: Test question"));
558        // Just check that it has some content - the specific format may vary
559        assert!(!brief.is_empty());
560    }
561
562    #[test]
563    fn test_format_full() {
564        let diviner = Diviner::new();
565        let reading = diviner
566            .cast_reading_from_numbers([7, 8, 7, 8, 7, 8], Some("Test question".to_string()))
567            .unwrap();
568
569        let full = format_full(&reading).unwrap();
570        assert!(full.contains("Question: Test question"));
571        assert!(full.contains("Traditional numbers"));
572        assert!(full.contains("Upper trigram"));
573        assert!(full.contains("Lower trigram"));
574    }
575
576    #[test]
577    fn test_parse_hexagram_number() {
578        let mut diviner = Diviner::new();
579        let reading = parse_input_and_create_reading(&mut diviner, "1").unwrap();
580        assert_eq!(reading.primary_hexagram(), 1);
581    }
582
583    #[test]
584    fn test_parse_line_numbers() {
585        let mut diviner = Diviner::new();
586        let reading = parse_input_and_create_reading(&mut diviner, "7,8,9,6,7,8").unwrap();
587        assert_eq!(reading.traditional_numbers(), [7, 8, 9, 6, 7, 8]);
588    }
589
590    #[test]
591    fn test_parse_unicode_character() {
592        let mut diviner = Diviner::new();
593        let reading = parse_input_and_create_reading(&mut diviner, "䷀").unwrap();
594        assert_eq!(reading.primary_hexagram(), 1);
595    }
596
597    #[test]
598    fn test_invalid_input() {
599        let mut diviner = Diviner::new();
600        assert!(parse_input_and_create_reading(&mut diviner, "65").is_err());
601        assert!(parse_input_and_create_reading(&mut diviner, "7,8,5,6,7,8").is_err());
602        assert!(parse_input_and_create_reading(&mut diviner, "invalid").is_err());
603    }
604
605    #[test]
606    fn test_parse_changing_hexagram_numbers() {
607        let mut diviner = Diviner::new();
608        let reading = parse_input_and_create_reading(&mut diviner, "32→34").unwrap();
609        assert_eq!(reading.primary_hexagram(), 32);
610        assert!(reading.has_changing_lines());
611        if let Some(transformed) = reading.transformed_hexagram() {
612            assert_eq!(transformed.primary_hexagram(), 34);
613        } else {
614            panic!("Expected transformed hexagram");
615        }
616    }
617
618    #[test]
619    fn test_parse_changing_hexagram_ascii_arrow() {
620        let mut diviner = Diviner::new();
621        let reading = parse_input_and_create_reading(&mut diviner, "1->2").unwrap();
622        assert_eq!(reading.primary_hexagram(), 1);
623        assert!(reading.has_changing_lines());
624        if let Some(transformed) = reading.transformed_hexagram() {
625            assert_eq!(transformed.primary_hexagram(), 2);
626        } else {
627            panic!("Expected transformed hexagram");
628        }
629    }
630
631    #[test]
632    fn test_parse_changing_hexagram_unicode() {
633        let mut diviner = Diviner::new();
634        let reading = parse_input_and_create_reading(&mut diviner, "䷀→䷁").unwrap();
635        assert_eq!(reading.primary_hexagram(), 1);
636        assert!(reading.has_changing_lines());
637        if let Some(transformed) = reading.transformed_hexagram() {
638            assert_eq!(transformed.primary_hexagram(), 2);
639        } else {
640            panic!("Expected transformed hexagram");
641        }
642    }
643}