ingrid_core/
grid_config.rs

1//! This module implements code for configuring a crossword-filling operation, independent of the
2//! specific fill algorithm.
3
4use smallvec::{smallvec, SmallVec};
5use std::collections::HashMap;
6use std::fmt::Debug;
7use std::sync::atomic::AtomicBool;
8use std::sync::Arc;
9
10#[cfg(feature = "serde")]
11use serde_derive::{Deserialize, Serialize};
12
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15
16use crate::types::{GlyphId, WordId};
17use crate::util::build_glyph_counts_by_cell;
18use crate::word_list::WordList;
19use crate::{MAX_SLOT_COUNT, MAX_SLOT_LENGTH};
20
21/// An identifier for the intersection between two slots; these correspond one-to-one with checked
22/// squares in the grid and are used to track weights (i.e., how often each square is involved in
23/// a domain wipeout).
24pub type CrossingId = usize;
25
26/// An identifier for a given slot, based on its index in the `GridConfig`'s `slot_configs` field.
27pub type SlotId = usize;
28
29/// Zero-indexed x and y coords for a cell in the grid, where y = 0 in the top row.
30pub type GridCoord = (usize, usize);
31
32/// The direction that a slot is facing.
33#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
36#[allow(dead_code)]
37pub enum Direction {
38    Across,
39    Down,
40}
41
42/// A struct representing a crossing between one slot and another, referencing the other slot's id
43/// and the location of the intersection within the other slot.
44#[derive(Debug, Clone)]
45pub struct Crossing {
46    pub other_slot_id: SlotId,
47    pub other_slot_cell: usize,
48    pub crossing_id: CrossingId,
49}
50
51/// A struct representing the aspects of a slot in the grid that are static during filling.
52#[derive(Debug, Clone)]
53pub struct SlotConfig {
54    pub id: SlotId,
55    pub start_cell: GridCoord,
56    pub direction: Direction,
57    pub length: usize,
58    pub crossings: SmallVec<[Option<Crossing>; MAX_SLOT_LENGTH]>,
59}
60
61impl SlotConfig {
62    /// Generate the coords for each cell of this slot.
63    #[must_use]
64    pub fn cell_coords(&self) -> Vec<GridCoord> {
65        (0..self.length)
66            .map(|cell_idx| match self.direction {
67                Direction::Across => (self.start_cell.0 + cell_idx, self.start_cell.1),
68                Direction::Down => (self.start_cell.0, self.start_cell.1 + cell_idx),
69            })
70            .collect()
71    }
72
73    /// Generate the indices of this slot's cells in a flat fill array like `GridConfig.fill`.
74    #[must_use]
75    pub fn cell_fill_indices(&self, grid_width: usize) -> Vec<usize> {
76        self.cell_coords()
77            .iter()
78            .map(|loc| loc.0 + loc.1 * grid_width)
79            .collect()
80    }
81
82    /// Get the values of this slot's cells in a flat fill array like `GridConfig.fill`.
83    #[must_use]
84    pub fn fill(&self, fill: &[Option<GlyphId>], grid_width: usize) -> Vec<Option<GlyphId>> {
85        self.cell_fill_indices(grid_width)
86            .iter()
87            .map(|&idx| fill[idx])
88            .collect()
89    }
90
91    /// Get this slot's `fill` if and only if all of its cells are populated.
92    #[must_use]
93    pub fn complete_fill(
94        &self,
95        fill: &[Option<GlyphId>],
96        grid_width: usize,
97    ) -> Option<Vec<GlyphId>> {
98        self.fill(fill, grid_width).into_iter().collect()
99    }
100
101    /// Generate a `SlotSpec` identifying this slot.
102    #[must_use]
103    pub fn slot_spec(&self) -> SlotSpec {
104        SlotSpec {
105            start_cell: self.start_cell,
106            direction: self.direction,
107            length: self.length,
108        }
109    }
110
111    /// Generate a string key identifying this slot.
112    #[must_use]
113    pub fn slot_key(&self) -> String {
114        self.slot_spec().to_key()
115    }
116}
117
118/// A struct holding references to all of the information needed as input to a crossword filling
119/// operation.
120#[allow(dead_code)]
121#[derive(Clone)]
122pub struct GridConfig<'a> {
123    /// The word list used to fill the grid; see `word_list.rs`.
124    pub word_list: &'a WordList,
125
126    /// A flat array of letters filled into the grid, in order of row and then column. `None` can
127    /// represent a block or an unfilled cell.
128    pub fill: &'a [Option<GlyphId>],
129
130    /// Config representing all of the slots in the grid and their crossings.
131    pub slot_configs: &'a [SlotConfig],
132
133    /// An array of available words for each (respective) slot, based on both the word list config
134    /// and the existing letters filled into the grid.
135    pub slot_options: &'a [Vec<WordId>],
136
137    /// The width and height of the grid.
138    pub width: usize,
139    pub height: usize,
140
141    /// The number of distinct crossings represented in all of the `slot_configs`.
142    pub crossing_count: usize,
143
144    /// An optional atomic flag that can be set to signal that the fill operation should be canceled.
145    pub abort: Option<&'a AtomicBool>,
146}
147
148/// A struct that owns a copy of each piece of information needed by `GridConfig`.
149pub struct OwnedGridConfig {
150    pub word_list: WordList,
151    pub fill: Vec<Option<GlyphId>>,
152    pub slot_configs: SmallVec<[SlotConfig; MAX_SLOT_COUNT]>,
153    pub slot_options: SmallVec<[Vec<WordId>; MAX_SLOT_COUNT]>,
154    pub width: usize,
155    pub height: usize,
156    pub crossing_count: usize,
157    pub abort: Option<Arc<AtomicBool>>,
158}
159
160impl OwnedGridConfig {
161    #[allow(dead_code)]
162    #[must_use]
163    pub fn to_config_ref(&self) -> GridConfig {
164        GridConfig {
165            word_list: &self.word_list,
166            fill: &self.fill,
167            slot_configs: &self.slot_configs,
168            slot_options: &self.slot_options,
169            width: self.width,
170            height: self.height,
171            crossing_count: self.crossing_count,
172            abort: self.abort.as_deref(),
173        }
174    }
175}
176
177/// Given a configured grid, reorder the options for each slot so that the "best" choices are at the
178/// front. This is a balance between fillability (the most important factor, since our odds of being
179/// able to find a fill in a reasonable amount of time depend on how many tries it takes us to find
180/// a usable word for each slot) and quality metrics like word score and letter score.
181#[allow(clippy::cast_lossless)]
182pub fn sort_slot_options(
183    word_list: &WordList,
184    slot_configs: &[SlotConfig],
185    slot_options: &mut SmallVec<[Vec<WordId>; MAX_SLOT_COUNT]>,
186) {
187    // To calculate the fillability score for each word, we need statistics about which letters are
188    // most likely to appear in each position for each slot.
189    let glyph_counts_by_cell_by_slot: Vec<_> = slot_configs
190        .iter()
191        .map(|slot_config| {
192            build_glyph_counts_by_cell(word_list, slot_config.length, &slot_options[slot_config.id])
193        })
194        .collect();
195
196    // Now we can actually sort the options.
197    for slot_idx in 0..slot_configs.len() {
198        let slot_config = &slot_configs[slot_idx];
199        let slot_options = &mut slot_options[slot_idx];
200
201        slot_options.sort_by_cached_key(|&option| {
202            let word = &word_list.words[slot_config.length][option];
203
204            // To calculate the fill score for a word, average the logarithms of the number of
205            // crossing options that are compatible with each letter (based on the grid geometry).
206            // This is kind of arbitrary, but it seems like it makes sense because we care a lot
207            // more about the difference between 1 option and 5 options or 5 options and 20 options
208            // than 100 options and 500 options.
209            let fill_score = slot_config
210                .crossings
211                .iter()
212                .zip(&word.glyphs)
213                .map(|(crossing, &glyph)| match crossing {
214                    Some(crossing) => {
215                        let crossing_counts_by_cell =
216                            &glyph_counts_by_cell_by_slot[crossing.other_slot_id];
217
218                        (crossing_counts_by_cell[crossing.other_slot_cell][glyph] as f32).log10()
219                    }
220                    None => 0.0,
221                })
222                .fold(0.0, |a, b| a + b)
223                / (slot_config.length as f32);
224
225            // This is arbitrary, based on visual inspection of the ranges for each value. Generally
226            // increasing the weight of `fill_score` relative to the other two will reduce fill
227            // time.
228            -((fill_score * 900.0) as i64
229                + ((word.letter_score as f32) * 5.0) as i64
230                + ((word.score as f32) * 5.0) as i64)
231        });
232    }
233}
234
235/// A struct identifying a specific slot in the grid.
236#[derive(Debug, PartialEq, Eq, Hash, Clone)]
237pub struct SlotSpec {
238    pub start_cell: GridCoord,
239    pub direction: Direction,
240    pub length: usize,
241}
242
243impl SlotSpec {
244    /// Parse a string like "1,2,down,5" into a `SlotSpec` struct.
245    pub fn from_key(key: &str) -> Result<SlotSpec, String> {
246        let key_parts: Vec<&str> = key.split(',').collect();
247        if key_parts.len() != 4 {
248            return Err(format!("invalid slot key: {key}"));
249        }
250
251        let x: Result<usize, _> = key_parts[0].parse();
252        let y: Result<usize, _> = key_parts[1].parse();
253        let direction: Option<Direction> = match key_parts[2] {
254            "across" => Some(Direction::Across),
255            "down" => Some(Direction::Down),
256            _ => None,
257        };
258        let length: Result<usize, _> = key_parts[3].parse();
259
260        if let (Ok(x), Ok(y), Some(direction), Ok(length)) = (x, y, direction, length) {
261            Ok(SlotSpec {
262                start_cell: (x, y),
263                direction,
264                length,
265            })
266        } else {
267            Err(format!("invalid slot key: {key:?}"))
268        }
269    }
270
271    /// Represent this slot as a string like "1,2,down,5".
272    #[must_use]
273    pub fn to_key(&self) -> String {
274        let direction = match self.direction {
275            Direction::Across => "across",
276            Direction::Down => "down",
277        };
278        format!(
279            "{},{},{},{}",
280            self.start_cell.0, self.start_cell.1, direction, self.length,
281        )
282    }
283
284    /// Does this spec match the given slot config?
285    #[must_use]
286    pub fn matches_slot(&self, slot: &SlotConfig) -> bool {
287        self.start_cell == slot.start_cell
288            && self.direction == slot.direction
289            && self.length == slot.length
290    }
291
292    /// Generate the coords for each cell of this entry.
293    #[must_use]
294    pub fn cell_coords(&self) -> Vec<GridCoord> {
295        (0..self.length)
296            .map(|cell_idx| match self.direction {
297                Direction::Across => (self.start_cell.0 + cell_idx, self.start_cell.1),
298                Direction::Down => (self.start_cell.0, self.start_cell.1 + cell_idx),
299            })
300            .collect()
301    }
302}
303
304/// Serialize a `SlotSpec` into a string key.
305#[cfg(feature = "serde")]
306impl Serialize for SlotSpec {
307    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
308    where
309        S: Serializer,
310    {
311        serializer.serialize_str(&self.to_key())
312    }
313}
314
315/// Deserialize a `SlotSpec` from a string key.
316#[cfg(feature = "serde")]
317impl<'de> Deserialize<'de> for SlotSpec {
318    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
319    where
320        D: Deserializer<'de>,
321    {
322        let raw_string = String::deserialize(deserializer)?;
323        SlotSpec::from_key(&raw_string).map_err(serde::de::Error::custom)
324    }
325}
326
327/// Given `GridEntry` structs specifying the positions of the slots in a grid, generate
328/// `SlotConfig`s containing derived information about crossings, etc.
329#[must_use]
330pub fn generate_slot_configs(
331    entries: &[SlotSpec],
332) -> (SmallVec<[SlotConfig; MAX_SLOT_COUNT]>, usize) {
333    #[derive(Debug)]
334    struct GridCell {
335        entries: Vec<(usize, usize)>, // (entry index, cell index within entry)
336        number: Option<u32>,
337    }
338
339    let mut slot_configs: SmallVec<[SlotConfig; MAX_SLOT_COUNT]> = smallvec![];
340
341    // Build a map from cell location to entries involved, which we can then use to calculate
342    // crossings.
343    let mut cell_by_loc: HashMap<GridCoord, GridCell> = HashMap::new();
344
345    for (entry_idx, entry) in entries.iter().enumerate() {
346        for (cell_idx, &loc) in entry.cell_coords().iter().enumerate() {
347            let grid_cell = cell_by_loc.entry(loc).or_insert_with(|| GridCell {
348                entries: vec![],
349                number: None,
350            });
351            grid_cell.entries.push((entry_idx, cell_idx));
352        }
353    }
354
355    let mut ordered_coords: Vec<_> = cell_by_loc.keys().copied().collect();
356    ordered_coords.sort_by_key(|&(x, y)| (y, x));
357    let mut current_number = 1;
358    for coord in ordered_coords {
359        if cell_by_loc[&coord]
360            .entries
361            .iter()
362            .any(|&(_, cell_idx)| cell_idx == 0)
363        {
364            cell_by_loc.get_mut(&coord).unwrap().number = Some(current_number);
365            current_number += 1;
366        }
367    }
368
369    // This is slightly tricky. When we're generating a Crossing, if
370    // `(current_slot_id, crossing_slot_id)` is in this list, use its index; if not, use
371    // `constraint_id_cache.len()` as the id and push `(crossing_slot_id, current_id)` into the list
372    // so we can reuse it when we see the crossing from the other side. This wouldn't work if the
373    // grid topology weren't 2D, so that each crossing is guaranteed to be seen by exactly two slots.
374    let mut constraint_id_cache: Vec<(SlotId, SlotId)> = vec![];
375
376    // Now we can build the actual slot configs.
377    for (entry_idx, entry) in entries.iter().enumerate() {
378        let crossings: SmallVec<[Option<Crossing>; MAX_SLOT_LENGTH]> = entry
379            .cell_coords()
380            .iter()
381            .map(|&loc| {
382                let crossing_idxs: Vec<_> = cell_by_loc[&loc]
383                    .entries
384                    .iter()
385                    .filter(|&&(e, _)| e != entry_idx)
386                    .collect();
387
388                if crossing_idxs.is_empty() {
389                    None
390                } else if crossing_idxs.len() > 1 {
391                    panic!("More than two entries crossing in cell?");
392                } else {
393                    let &(other_slot_id, other_slot_cell) = crossing_idxs[0];
394
395                    let crossing_id = if let Some(found_constraint_id) = constraint_id_cache
396                        .iter()
397                        .enumerate()
398                        .find(|&(_, &id_pair)| id_pair == (entry_idx, other_slot_id))
399                        .map(|(crossing_id, _)| crossing_id)
400                    {
401                        found_constraint_id
402                    } else {
403                        constraint_id_cache.push((other_slot_id, entry_idx));
404                        constraint_id_cache.len() - 1
405                    };
406
407                    Some(Crossing {
408                        other_slot_id,
409                        other_slot_cell,
410                        crossing_id,
411                    })
412                }
413            })
414            .collect();
415
416        slot_configs.push(SlotConfig {
417            id: entry_idx,
418            start_cell: entry.start_cell,
419            direction: entry.direction,
420            length: entry.length,
421            crossings,
422        });
423    }
424
425    (slot_configs, constraint_id_cache.len())
426}
427
428/// Given an input fill and an an array of slot configs, generate the possible options for each
429/// slot by starting with the complete word list and then removing words that contradict any fill
430/// that's already present in the grid.
431pub fn generate_slot_options(
432    word_list: &mut WordList,
433    fill: &[Option<GlyphId>],
434    slot_configs: &[SlotConfig],
435    grid_width: usize,
436    min_score: u16,
437) -> SmallVec<[Vec<WordId>; MAX_SLOT_COUNT]> {
438    let mut slot_options: SmallVec<[Vec<WordId>; MAX_SLOT_COUNT]> = smallvec![];
439
440    for slot in slot_configs {
441        let entry_fill = slot.fill(fill, grid_width);
442
443        // If the slot is fully specified, we need to either use an existing word or create a new
444        // (hidden) one.
445        let complete_fill: Option<SmallVec<[GlyphId; MAX_SLOT_COUNT]>> =
446            entry_fill.iter().copied().collect();
447
448        if let Some(complete_fill) = complete_fill {
449            let word_string: String = complete_fill
450                .iter()
451                .map(|&glyph_id| word_list.glyphs[glyph_id])
452                .collect();
453
454            let (_word_length, word_id) = word_list.get_word_id_or_add_hidden(&word_string);
455
456            slot_options.push(vec![word_id]);
457        } else {
458            let options: Vec<WordId> = (0..word_list.words[slot.length].len())
459                .filter(|&word_id| {
460                    let word = &word_list.words[slot.length][word_id];
461                    if word.hidden || word.score < min_score {
462                        return false;
463                    }
464
465                    entry_fill.iter().enumerate().all(|(cell_idx, cell_fill)| {
466                        cell_fill
467                            .map(|g| g == word.glyphs[cell_idx])
468                            .unwrap_or(true)
469                    })
470                })
471                .collect();
472
473            slot_options.push(options);
474        }
475    }
476
477    slot_options
478}
479
480/// Generate an `OwnedGridConfig` representing a grid with specified entries.
481#[must_use]
482pub fn generate_grid_config<'a>(
483    mut word_list: WordList,
484    entries: &'a [SlotSpec],
485    raw_fill: &'a [Option<String>],
486    width: usize,
487    height: usize,
488    min_score: u16,
489) -> OwnedGridConfig {
490    let (slot_configs, crossing_count) = generate_slot_configs(entries);
491
492    let fill: Vec<Option<GlyphId>> = raw_fill
493        .iter()
494        .map(|cell_str| {
495            cell_str
496                .as_ref()
497                .map(|cell_str| word_list.glyph_id_for_char(cell_str.chars().next().unwrap()))
498        })
499        .collect();
500
501    let mut slot_options =
502        generate_slot_options(&mut word_list, &fill, &slot_configs, width, min_score);
503
504    sort_slot_options(&word_list, &slot_configs, &mut slot_options);
505
506    OwnedGridConfig {
507        word_list,
508        fill,
509        slot_configs,
510        slot_options,
511        width,
512        height,
513        crossing_count,
514        abort: None,
515    }
516}
517
518/// Generate a list of `SlotSpec`s from a template string with . representing empty cells, # representing
519/// blocks, and letters representing themselves.
520#[allow(dead_code)]
521#[must_use]
522pub fn generate_slots_from_template_string(template: &str) -> Vec<SlotSpec> {
523    fn build_words(template: &[Vec<char>]) -> Vec<Vec<GridCoord>> {
524        let mut result: Vec<Vec<GridCoord>> = vec![];
525
526        for (y, line) in template.iter().enumerate() {
527            let mut current_word_coords: Vec<GridCoord> = vec![];
528
529            for (x, &cell) in line.iter().enumerate() {
530                if cell == '#' {
531                    if current_word_coords.len() > 1 {
532                        result.push(current_word_coords);
533                    }
534                    current_word_coords = vec![];
535                } else {
536                    current_word_coords.push((x, y));
537                }
538            }
539
540            if current_word_coords.len() > 1 {
541                result.push(current_word_coords);
542            }
543        }
544
545        result
546    }
547
548    let template: Vec<Vec<char>> = template
549        .lines()
550        .filter_map(|line| {
551            let line = line.trim();
552            if line.is_empty() {
553                None
554            } else {
555                Some(line.chars().collect())
556            }
557        })
558        .collect();
559
560    let mut slot_specs: Vec<SlotSpec> = vec![];
561
562    for coords in build_words(&template) {
563        slot_specs.push(SlotSpec {
564            start_cell: coords[0],
565            length: coords.len(),
566            direction: Direction::Across,
567        });
568    }
569
570    let transposed_template: Vec<Vec<char>> = (0..template[0].len())
571        .map(|y| (0..template.len()).map(|x| template[x][y]).collect())
572        .collect();
573
574    for coords in build_words(&transposed_template) {
575        let coords: Vec<GridCoord> = coords.iter().copied().map(|(y, x)| (x, y)).collect();
576        slot_specs.push(SlotSpec {
577            start_cell: coords[0],
578            length: coords.len(),
579            direction: Direction::Down,
580        });
581    }
582
583    slot_specs
584}
585
586/// Generate an `OwnedGridConfig` from a template string with . representing empty cells, # representing
587/// blocks, and letters representing themselves.
588#[allow(dead_code)]
589#[must_use]
590pub fn generate_grid_config_from_template_string(
591    word_list: WordList,
592    template: &str,
593    min_score: u16,
594) -> OwnedGridConfig {
595    let slot_specs = generate_slots_from_template_string(template);
596
597    let fill: Vec<Vec<Option<String>>> = template
598        .lines()
599        .filter_map(|line| {
600            let line = line.trim();
601            if line.is_empty() {
602                None
603            } else {
604                Some(
605                    line.chars()
606                        .map(|c| {
607                            if c == '.' || c == '#' {
608                                None
609                            } else {
610                                Some(c.to_lowercase().to_string())
611                            }
612                        })
613                        .collect(),
614                )
615            }
616        })
617        .collect();
618
619    let width = fill[0].len();
620    let height = fill.len();
621
622    generate_grid_config(
623        word_list,
624        &slot_specs,
625        &fill.into_iter().flatten().collect::<Vec<_>>(),
626        width,
627        height,
628        min_score,
629    )
630}
631
632/// A struct recording a slot assignment made during a fill process.
633#[derive(Debug, Clone)]
634pub struct Choice {
635    pub slot_id: SlotId,
636    pub word_id: WordId,
637}
638
639/// Turn the given grid config and fill choices into a rendered string.
640#[allow(dead_code)]
641#[must_use]
642pub fn render_grid(config: &GridConfig, choices: &[Choice]) -> String {
643    let max_x = config
644        .slot_configs
645        .iter()
646        .map(|slot_config| match slot_config.direction {
647            Direction::Across => slot_config.start_cell.0 + slot_config.length - 1,
648            Direction::Down => slot_config.start_cell.0,
649        })
650        .max()
651        .expect("Grid must have slots");
652
653    let max_y = config
654        .slot_configs
655        .iter()
656        .map(|slot_config| match slot_config.direction {
657            Direction::Across => slot_config.start_cell.1,
658            Direction::Down => slot_config.start_cell.1 + slot_config.length - 1,
659        })
660        .max()
661        .expect("Grid must have slots");
662
663    let mut grid: Vec<Vec<Option<char>>> = (0..=max_y)
664        .map(|_| (0..=max_x).map(|_| None).collect::<Vec<_>>())
665        .collect();
666
667    for &Choice { slot_id, word_id } in choices {
668        let slot_config = &config.slot_configs[slot_id];
669        let word = &config.word_list.words[slot_config.length][word_id];
670
671        for (cell_idx, &glyph) in word.glyphs.iter().enumerate() {
672            let (x, y) = match slot_config.direction {
673                Direction::Across => (
674                    slot_config.start_cell.0 + cell_idx,
675                    slot_config.start_cell.1,
676                ),
677                Direction::Down => (
678                    slot_config.start_cell.0,
679                    slot_config.start_cell.1 + cell_idx,
680                ),
681            };
682
683            grid[y][x] = Some(config.word_list.glyphs[glyph]);
684        }
685    }
686
687    grid.iter()
688        .map(|line| {
689            line.iter()
690                .map(|cell| cell.unwrap_or('.').to_string())
691                .collect::<String>()
692        })
693        .collect::<Vec<_>>()
694        .join("\n")
695}
696
697#[cfg(all(test, feature = "serde"))]
698mod serde_tests {
699    use crate::grid_config::{Direction, SlotSpec};
700
701    #[test]
702    fn test_slot_spec_serialization() {
703        let slot_spec = SlotSpec {
704            start_cell: (1, 2),
705            direction: Direction::Across,
706            length: 5,
707        };
708
709        let slot_key = serde_json::to_string(&slot_spec).unwrap();
710
711        assert_eq!(slot_key, "\"1,2,across,5\"");
712    }
713
714    #[test]
715    fn test_slot_spec_deserialization() {
716        let slot_spec: SlotSpec = serde_json::from_str("\"3,4,down,12\"").unwrap();
717
718        assert_eq!(
719            slot_spec,
720            SlotSpec {
721                start_cell: (3, 4),
722                direction: Direction::Down,
723                length: 12,
724            }
725        );
726    }
727}