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 #[arg(short, long, default_value = "full")]
56 pub format: Format,
57
58 #[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 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
105fn parse_input_and_create_reading(diviner: &mut Diviner, input: &str) -> Result<Reading> {
107 let input = input.trim();
108
109 if let Some(reading) = try_parse_changing_hexagram(input)? {
112 return Ok(reading);
113 }
114
115 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 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 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
151fn try_parse_changing_hexagram(input: &str) -> Result<Option<Reading>> {
153 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 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 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
192fn 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 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
209fn create_reading_from_hexagram_number(hexagram_number: u8) -> Result<Reading> {
211 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 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
236fn create_changing_reading_from_numbers(from_hexagram: u8, to_hexagram: u8) -> Result<Reading> {
238 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 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 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 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
303fn 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 result.push_str(&format!(
432 "\nTraditional numbers: {:?}\n",
433 reading.traditional_numbers()
434 ));
435
436 result.push_str(&format!("Upper trigram: {:?}\n", reading.upper_trigram()));
438 result.push_str(&format!("Lower trigram: {:?}\n", reading.lower_trigram()));
439
440 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 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 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 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 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}