Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.1.098.9 KB2026-09-14
Version
0.1.0
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-14
Size
98.9 KB
Downloads
1
Checksum
bdac84a1e4fe4dd3505b93c2781b1f15833b5189c2e4ee976225fa3c512885cc
Dependencies
None

shared.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! 共有文字列
//!
//! xlsx は、セルに入る文字をシートに直接は書かず、`xl/sharedStrings.xml` に一度だけ置いて、
//! セルからは番号で指す。同じ語が何度も出る表では、これでファイルがかなり小さくなる。
//!
//! # 置き方
//!
//! 文字列ごとに String を持つと、1000 万語で 1000 万個の入れ物ができる。
//! ここでは全部を 1 本の String につなぎ、始まりの位置だけを並べて持つ。
//! 1 語あたり 4 バイトで済み、入れ物を作る手間もかからない。

use crate::error::Error;
use crate::xml::{Event, Reader};
use std::collections::HashMap;


/// 共有をやめる文字の量
const FULL_TEXT: usize = 64 * 1024 * 1024;

/// 共有をやめる語の数
const FULL_COUNT: usize = 1 << 20;


/// 共有文字列の表
pub struct Shared {
    /// 文字を全部つないだもの
    text: String,
    /// 語ごとの始まり。末尾に text の長さを置くので、要素数は語数 + 1
    start: Vec<u32>,
    /// 書くときに、同じ語を二度入れないための引き
    place: HashMap<String, u32>,
}


impl Shared {
    /// 空の表
    pub fn new() -> Shared {
        Shared {text: String::new(), start: vec![0], place: HashMap::new()}
    }

    /// sharedStrings.xml を読む
    ///
    /// `<si>` が 1 語である。`<r>` で区切られた飾りつきの語は、つないで 1 語にする。
    /// `<rPh>` はふりがななので、中の `<t>` は語に入れない。
    pub fn read(reader: &mut Reader) -> Result<Shared, Error> {
        let mut result = Shared::new();
        let mut inside = false;
        let mut reading = false;
        let mut phonetic = 0;

        while let Some(event) = reader.next()? {
            match event {
                Event::Open => match reader.name() {
                    "si" => inside = true,
                    "rPh" => phonetic += 1,
                    "t" => reading = inside && phonetic == 0,
                    _ => {}
                },

                Event::Close => match reader.name() {
                    "si" => {
                        if inside {
                            result.start.push(result.text.len() as u32);
                            inside = false;
                        }
                    }
                    "rPh" => phonetic -= 1,
                    "t" => reading = false,
                    _ => {}
                },

                Event::Text => {
                    if reading {
                        result.text.push_str(reader.text());
                    }
                }

                Event::Empty => {}
            }
        }

        Ok(result)
    }

    /// 入っている語の数
    pub fn count(&self) -> usize {
        self.start.len() - 1
    }

    /// 番号で引く。範囲の外なら空
    pub fn get(&self, index: u32) -> &str {
        let index = index as usize;

        if index + 1 >= self.start.len() {
            return "";
        }

        &self.text[self.start[index] as usize..self.start[index + 1] as usize]
    }

    /// 表がこれ以上ふくらむと困る大きさに達したか
    ///
    /// 共有文字列は同じ語をまとめるので、ふつうはファイルが小さくなる。
    /// ただし語がすべて違う巨大なシートでは、表がそのままメモリに残り続ける。
    /// ここを超えたら、書く側は共有をやめてセルに直に書く。
    pub fn is_full(&self) -> bool {
        self.text.len() >= FULL_TEXT || self.count() >= FULL_COUNT
    }

    /// 同じ語をまとめずに足す
    ///
    /// 旧形式の共有文字列は番号で指されているので、重なりをまとめると番号がずれる。
    /// 読むときはこちらを使う。
    pub fn push(&mut self, value: &str) {
        self.text.push_str(value);
        self.start.push(self.text.len() as u32);
    }

    /// 語を入れて番号を返す。すでにあれば、その番号を返す
    pub fn put(&mut self, value: &str) -> u32 {
        if let Some(index) = self.place.get(value) {
            return *index;
        }

        let index = self.count() as u32;
        self.text.push_str(value);
        self.start.push(self.text.len() as u32);
        self.place.insert(value.to_string(), index);
        index
    }
}


impl Default for Shared {
    fn default() -> Shared {
        Shared::new()
    }
}


/// `_xHHHH_` の形を文字に戻す
///
/// XML に置けない文字を、Excel はこの形で書く。左から順に見るので、
/// 文字として `_x0041_` と書きたいときに Excel が出す `_x005F_x0041_` も、正しく戻る。
pub fn decode(text: &str) -> String {
    if !text.contains("_x") {
        return text.to_string();
    }

    let byte = text.as_bytes();
    let mut result = String::with_capacity(text.len());
    let mut cursor = 0;

    while cursor < byte.len() {
        if byte[cursor] == b'_' && cursor + 7 <= byte.len() && byte[cursor + 1] == b'x' && byte[cursor + 6] == b'_' {
            let digit = &text[cursor + 2..cursor + 6];

            if let Ok(number) = u32::from_str_radix(digit, 16) {
                if let Some(value) = char::from_u32(number) {
                    result.push(value);
                    cursor += 7;
                    continue;
                }
            }
        }

        let step = step_of(byte[cursor]);
        result.push_str(&text[cursor..cursor + step]);
        cursor += step;
    }

    result
}


/// XML に置けない文字を `_xHHHH_` の形にする
pub fn encode(text: &str) -> String {
    let mut result = String::with_capacity(text.len());

    for value in text.chars() {
        let code = value as u32;

        if code < 0x20 && value != '\t' && value != '\n' && value != '\r' {
            result.push_str(&format!("_x{code:04X}_"));
            continue;
        }

        result.push(value);
    }

    result
}


/// その位置から始まる文字のバイト数
fn step_of(first: u8) -> usize {
    match first {
        0x00..=0x7f => 1,
        0xc0..=0xdf => 2,
        0xe0..=0xef => 3,
        _ => 4,
    }
}