csan 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bff45dc4e1ef8a3625bc4d045b099fcd6b34b800fda8845b5c353bd6a3a0012f
4
- data.tar.gz: 0add5aeddb5955fd080a744c4dd06f5e2fc21d3eb0e45fcd7998352867afc1ea
3
+ metadata.gz: 4d3f833779c060221fc8d413bf0897ccebc862e2cb381329aabdba91abcfc4f3
4
+ data.tar.gz: 6d9ec7c4dc9d4e7b87ddebf1bd4fae83a081b3f148ab3c95e8fe4c51845b8f43
5
5
  SHA512:
6
- metadata.gz: b58f02e859fb6a0dfa38f06560773440463f7093cc759d9229fcb964e0cf6fbf3ee6b6f798853c273849a58de28fb59b57e2b752ffe7a99c41bb0e0f691c93cf
7
- data.tar.gz: a868b1fa3f498113763c757f03a997d06256d795a9b63f76d08d6ad6470e4bcc71702285c5fcf0d5a476c957715f02f831649741c1e272ece6f030863d692608
6
+ metadata.gz: 9454f8bc0d9ea568c8aae12af1a0e6e4fd342acc15381b9ced4de0f8835738c0e481cfe0e8be0c0007ce363b05d1da9689f34cf92a32053386f0f94d78d5dd88
7
+ data.tar.gz: 840755e651cbbb86a5b64ecb4ad8aec5f87e1acaa6128d1f729374e08b72d48b18bfc6be30ea3106bfb9b995c6cf7abcb98a4d0e8a86646753614b8741fc536d
data/README.md CHANGED
@@ -37,6 +37,22 @@ raises `ArgumentError`; a length that is not an Integer raises `TypeError`,
37
37
  and a negative one `RangeError`. [`sig/csan.rbs`](https://github.com/veralvx/csan/blob/main/crates/csan-rb/sig/csan.rbs)
38
38
  holds the type signatures.
39
39
 
40
+ Every function also takes `lang` and `strip_article`: with a truthy
41
+ `strip_article`, the title is filed without its initial article in the
42
+ language `lang` names (an ISO 639 code, a BCP 47 tag or a POSIX locale,
43
+ case-insensitive; `csan --help` lists them all). `lang` alone changes
44
+ nothing, a blank `lang` counts as absent, and author names are never
45
+ stripped. An unknown `lang` (even with `strip_article` off), or
46
+ `strip_article` without `lang`, raises `ArgumentError`, and a `lang` that
47
+ is not a String `TypeError`.
48
+
49
+ ```ruby
50
+ Csan.cutter_sanborn(surname: "Dickens", title: "A Christmas Carol", lang: "en", strip_article: true)
51
+ # => "D548c"
52
+ Csan.cutter_loc(title: "The Bear", lang: "en", strip_article: true)
53
+ # => "B437"
54
+ ```
55
+
40
56
  The executable is the same interface as the standalone Rust binary
41
57
  (options, help, messages, and exit codes):
42
58
 
data/ext/csan/Cargo.lock CHANGED
@@ -168,9 +168,9 @@ dependencies = [
168
168
 
169
169
  [[package]]
170
170
  name = "csan"
171
- version = "0.8.0"
171
+ version = "0.9.0"
172
172
  source = "registry+https://github.com/rust-lang/crates.io-index"
173
- checksum = "cb15b7a24bcce5da647fb7edeb5af6ccc1cc3a6fe50f2ccf93d6fed32ec060a3"
173
+ checksum = "faf7f4236511169f63c8b2db962c8d0dd472fcfac9ea347ae96e43d18689e8ec"
174
174
  dependencies = [
175
175
  "clap",
176
176
  "csan-core",
@@ -179,9 +179,9 @@ dependencies = [
179
179
 
180
180
  [[package]]
181
181
  name = "csan-core"
182
- version = "0.8.0"
182
+ version = "0.9.0"
183
183
  source = "registry+https://github.com/rust-lang/crates.io-index"
184
- checksum = "c05481502522d9e78dc91a5af3dedafc0c94f106e2f550c938144c8c6ffc36f2"
184
+ checksum = "56070a56f6dfc10c0367cfcd36886b6f4787b0ae0420d5e80c455146447f1570"
185
185
  dependencies = [
186
186
  "derive_more",
187
187
  "serde",
@@ -190,7 +190,7 @@ dependencies = [
190
190
 
191
191
  [[package]]
192
192
  name = "csan-rb"
193
- version = "0.8.0"
193
+ version = "0.9.0"
194
194
  dependencies = [
195
195
  "csan",
196
196
  "csan-core",
data/ext/csan/Cargo.toml CHANGED
@@ -13,7 +13,7 @@
13
13
  edition = "2024"
14
14
  rust-version = "1.97"
15
15
  name = "csan-rb"
16
- version = "0.8.0"
16
+ version = "0.9.0"
17
17
  authors = ["VERALVX <veralvx@veralvx.com>"]
18
18
  build = false
19
19
  publish = false
@@ -38,10 +38,10 @@ test = false
38
38
  doctest = false
39
39
 
40
40
  [dependencies.csan]
41
- version = "=0.8.0"
41
+ version = "=0.9.0"
42
42
 
43
43
  [dependencies.csan-core]
44
- version = "0.8.0"
44
+ version = "0.9.0"
45
45
 
46
46
  [dependencies.magnus]
47
47
  version = "0.9.3"
data/ext/csan/src/lib.rs CHANGED
@@ -16,26 +16,47 @@ use core::fmt;
16
16
  use core::iter;
17
17
  use csan_core::{
18
18
  Attribution, CutterConfig, CutterGenerator as _, CutterLoc, CutterLocMethod, CutterSanborn,
19
- CutterSanbornMethod, detailed_json,
19
+ CutterSanbornMethod, Language, MissingLanguage, detailed_json,
20
20
  };
21
21
  use magnus::value::ReprValue as _;
22
22
  use magnus::{Error, IntoValue as _, RHash, Ruby, Value};
23
23
  use serde_json::Value as Json;
24
24
 
25
- /// Builds the [`Attribution`] for a set of optional inputs, or Ruby's
26
- /// `ArgumentError`.
25
+ /// The language the title is filed in, from the `lang` and
26
+ /// `strip_article` arguments: `None` when the switch is off.
27
+ ///
28
+ /// `lang` is parsed even with the switch off, so a bad code is always an
29
+ /// `ArgumentError`; a blank `lang` counts as absent. The exported functions
30
+ /// call it before checking their inputs, so a language error comes first,
31
+ /// as in the CLI.
32
+ fn article_language(
33
+ ruby: &Ruby,
34
+ lang: Option<&str>,
35
+ strip_article: bool,
36
+ ) -> Result<Option<Language>, Error> {
37
+ let language = Language::from_optional(lang)
38
+ .map_err(|error| Error::new(ruby.exception_arg_error(), error.to_string()))?;
39
+ Language::for_rule(language, strip_article).map_err(|MissingLanguage| {
40
+ Error::new(ruby.exception_arg_error(), "strip_article requires lang")
41
+ })
42
+ }
43
+
44
+ /// Builds the [`Attribution`] for a set of optional inputs, with its title
45
+ /// filed in `article_language`, or Ruby's `ArgumentError`.
27
46
  fn attribution(
28
47
  ruby: &Ruby,
29
48
  forename: Option<&str>,
30
49
  surname: Option<&str>,
31
50
  title: Option<&str>,
51
+ article_language: Option<Language>,
32
52
  ) -> Result<Attribution, Error> {
33
- Attribution::from_parts(forename, surname, title).ok_or_else(|| {
53
+ let entry = Attribution::from_parts(forename, surname, title).ok_or_else(|| {
34
54
  Error::new(
35
55
  ruby.exception_arg_error(),
36
56
  "provide at least one non-blank input: forename, surname, or title",
37
57
  )
38
- })
58
+ })?;
59
+ Ok(entry.with_article_language(article_language))
39
60
  }
40
61
 
41
62
  /// Maps a failure to Ruby's `ArgumentError`, prefixed with `context`.
@@ -52,8 +73,9 @@ fn sanborn(
52
73
  title: Option<&str>,
53
74
  prefix_len: Option<usize>,
54
75
  suffix_len: Option<usize>,
76
+ article_language: Option<Language>,
55
77
  ) -> Result<CutterSanborn, Error> {
56
- let entry = attribution(ruby, forename, surname, title)?;
78
+ let entry = attribution(ruby, forename, surname, title, article_language)?;
57
79
  let config = CutterConfig::from_overrides(prefix_len, suffix_len, None);
58
80
  CutterSanbornMethod::generate(&entry, config)
59
81
  .map_err(|error| argument_error(ruby, "cutter generation failed", &error))
@@ -66,8 +88,9 @@ fn loc(
66
88
  surname: Option<&str>,
67
89
  title: Option<&str>,
68
90
  max_len: Option<usize>,
91
+ article_language: Option<Language>,
69
92
  ) -> Result<CutterLoc, Error> {
70
- let entry = attribution(ruby, forename, surname, title)?;
93
+ let entry = attribution(ruby, forename, surname, title, article_language)?;
71
94
  let config = CutterConfig::from_overrides(None, None, max_len);
72
95
  CutterLocMethod::generate(&entry, config)
73
96
  .map_err(|error| argument_error(ruby, "cutter generation failed", &error))
@@ -133,6 +156,10 @@ fn detailed_hash(ruby: &Ruby, value: &Json) -> Result<RHash, Error> {
133
156
  clippy::needless_pass_by_value,
134
157
  reason = "magnus passes Ruby's arguments as owned values"
135
158
  )]
159
+ #[expect(
160
+ clippy::too_many_arguments,
161
+ reason = "one parameter per Ruby argument, plus the interpreter handle"
162
+ )]
136
163
  fn cutter_sanborn(
137
164
  ruby: &Ruby,
138
165
  forename: Option<String>,
@@ -140,7 +167,10 @@ fn cutter_sanborn(
140
167
  title: Option<String>,
141
168
  prefix_len: Option<usize>,
142
169
  suffix_len: Option<usize>,
170
+ lang: Option<String>,
171
+ strip_article: bool,
143
172
  ) -> Result<String, Error> {
173
+ let language = article_language(ruby, lang.as_deref(), strip_article)?;
144
174
  let cutter = sanborn(
145
175
  ruby,
146
176
  forename.as_deref(),
@@ -148,6 +178,7 @@ fn cutter_sanborn(
148
178
  title.as_deref(),
149
179
  prefix_len,
150
180
  suffix_len,
181
+ language,
151
182
  )?;
152
183
  Ok(cutter.to_string())
153
184
  }
@@ -158,6 +189,10 @@ fn cutter_sanborn(
158
189
  clippy::needless_pass_by_value,
159
190
  reason = "magnus passes Ruby's arguments as owned values"
160
191
  )]
192
+ #[expect(
193
+ clippy::too_many_arguments,
194
+ reason = "one parameter per Ruby argument, plus the interpreter handle"
195
+ )]
161
196
  fn cutter_sanborn_detailed(
162
197
  ruby: &Ruby,
163
198
  forename: Option<String>,
@@ -165,7 +200,10 @@ fn cutter_sanborn_detailed(
165
200
  title: Option<String>,
166
201
  prefix_len: Option<usize>,
167
202
  suffix_len: Option<usize>,
203
+ lang: Option<String>,
204
+ strip_article: bool,
168
205
  ) -> Result<RHash, Error> {
206
+ let language = article_language(ruby, lang.as_deref(), strip_article)?;
169
207
  let cutter = sanborn(
170
208
  ruby,
171
209
  forename.as_deref(),
@@ -173,6 +211,7 @@ fn cutter_sanborn_detailed(
173
211
  title.as_deref(),
174
212
  prefix_len,
175
213
  suffix_len,
214
+ language,
176
215
  )?;
177
216
  let value = detailed_json(&cutter)
178
217
  .map_err(|error| argument_error(ruby, "detailed cutter value failed", &error))?;
@@ -190,13 +229,17 @@ fn cutter_loc(
190
229
  surname: Option<String>,
191
230
  title: Option<String>,
192
231
  max_len: Option<usize>,
232
+ lang: Option<String>,
233
+ strip_article: bool,
193
234
  ) -> Result<String, Error> {
235
+ let language = article_language(ruby, lang.as_deref(), strip_article)?;
194
236
  let cutter = loc(
195
237
  ruby,
196
238
  forename.as_deref(),
197
239
  surname.as_deref(),
198
240
  title.as_deref(),
199
241
  max_len,
242
+ language,
200
243
  )?;
201
244
  Ok(cutter.to_string())
202
245
  }
@@ -213,13 +256,17 @@ fn cutter_loc_detailed(
213
256
  surname: Option<String>,
214
257
  title: Option<String>,
215
258
  max_len: Option<usize>,
259
+ lang: Option<String>,
260
+ strip_article: bool,
216
261
  ) -> Result<RHash, Error> {
262
+ let language = article_language(ruby, lang.as_deref(), strip_article)?;
217
263
  let cutter = loc(
218
264
  ruby,
219
265
  forename.as_deref(),
220
266
  surname.as_deref(),
221
267
  title.as_deref(),
222
268
  max_len,
269
+ language,
223
270
  )?;
224
271
  let value = detailed_json(&cutter)
225
272
  .map_err(|error| argument_error(ruby, "detailed cutter value failed", &error))?;
@@ -249,13 +296,13 @@ mod entry {
249
296
  #[magnus::init]
250
297
  fn init(ruby: &Ruby) -> Result<(), Error> {
251
298
  let native = ruby.define_module("Csan")?.define_module("Native")?;
252
- native.define_module_function("cutter_sanborn", function!(cutter_sanborn, 5))?;
299
+ native.define_module_function("cutter_sanborn", function!(cutter_sanborn, 7))?;
253
300
  native.define_module_function(
254
301
  "cutter_sanborn_detailed",
255
- function!(cutter_sanborn_detailed, 5),
302
+ function!(cutter_sanborn_detailed, 7),
256
303
  )?;
257
- native.define_module_function("cutter_loc", function!(cutter_loc, 4))?;
258
- native.define_module_function("cutter_loc_detailed", function!(cutter_loc_detailed, 4))?;
304
+ native.define_module_function("cutter_loc", function!(cutter_loc, 6))?;
305
+ native.define_module_function("cutter_loc_detailed", function!(cutter_loc_detailed, 6))?;
259
306
  native.define_module_function("run_cli", function!(run_cli, 1))?;
260
307
  native.const_set("VERSION", env!("CARGO_PKG_VERSION"))?;
261
308
  Ok(())
data/lib/csan/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  module Csan
4
4
  # The gem's version: the csan workspace's, which `just release` sets here
5
5
  # too (and the extension reports as Csan::Native::VERSION).
6
- VERSION = "0.8.0"
6
+ VERSION = "0.9.0"
7
7
  end
data/lib/csan.rb CHANGED
@@ -23,10 +23,21 @@ end
23
23
  # that is not an Integer raises `TypeError` (a Float is truncated), and a
24
24
  # negative one `RangeError`.
25
25
  #
26
+ # Every function also takes `lang` and `strip_article`: with a truthy
27
+ # `strip_article`, the title is filed without its initial article in the
28
+ # language `lang` names, an ISO 639 code, a BCP 47 tag or a POSIX locale
29
+ # (`"en"`, `"pt-BR"`), case-insensitive; `csan --help` lists them all.
30
+ # `lang` alone changes nothing, a blank `lang` counts as absent, and author
31
+ # names are never stripped. An unknown `lang` (even with `strip_article`
32
+ # off), or `strip_article` without `lang`, raises `ArgumentError`, and a
33
+ # `lang` that is not a String `TypeError`.
34
+ #
26
35
  # @example
27
36
  # Csan.cutter_sanborn(forename: "Charles", surname: "Dickens", title: "Oliver Twist")
28
37
  # # => "D548o"
29
38
  # Csan.cutter_loc(surname: "Carter") # => "C378"
39
+ # Csan.cutter_sanborn(surname: "Dickens", title: "A Christmas Carol", lang: "en", strip_article: true)
40
+ # # => "D548c"
30
41
  module Csan
31
42
  # The Cutter-Sanborn three-figure number.
32
43
  #
@@ -38,10 +49,15 @@ module Csan
38
49
  # number (default 1)
39
50
  # @param suffix_len [Integer, nil] letters of the title in the work mark
40
51
  # (default 1)
52
+ # @param lang [String, nil] the work's language, for `strip_article`
53
+ # @param strip_article [Boolean, nil] file the title without its initial
54
+ # article in `lang` when truthy (default false)
41
55
  # @return [String] the number, e.g. "D548o"
42
- # @raise [ArgumentError] on unusable input
43
- def self.cutter_sanborn(forename: nil, surname: nil, title: nil, prefix_len: nil, suffix_len: nil)
44
- Native.cutter_sanborn(forename, surname, title, prefix_len, suffix_len)
56
+ # @raise [ArgumentError] on unusable input, an unknown `lang`, or
57
+ # `strip_article` without `lang`
58
+ def self.cutter_sanborn(forename: nil, surname: nil, title: nil, prefix_len: nil, suffix_len: nil,
59
+ lang: nil, strip_article: false)
60
+ Native.cutter_sanborn(forename, surname, title, prefix_len, suffix_len, lang, strip_article)
45
61
  end
46
62
 
47
63
  # The Cutter-Sanborn number's parts.
@@ -51,9 +67,10 @@ module Csan
51
67
  # @return [Hash{Symbol => String, Integer, nil}] `:alpha_prefix`,
52
68
  # `:numeric`, `:alpha_suffix` (nil without a work mark), `:entry` (the
53
69
  # table entry that matched), and `:formatted`
54
- # @raise [ArgumentError] on unusable input
55
- def self.cutter_sanborn_detailed(forename: nil, surname: nil, title: nil, prefix_len: nil, suffix_len: nil)
56
- Native.cutter_sanborn_detailed(forename, surname, title, prefix_len, suffix_len)
70
+ # @raise [ArgumentError] as {cutter_sanborn} does
71
+ def self.cutter_sanborn_detailed(forename: nil, surname: nil, title: nil, prefix_len: nil, suffix_len: nil,
72
+ lang: nil, strip_article: false)
73
+ Native.cutter_sanborn_detailed(forename, surname, title, prefix_len, suffix_len, lang, strip_article)
57
74
  end
58
75
 
59
76
  # The Library of Congress cutter number.
@@ -63,10 +80,15 @@ module Csan
63
80
  # @param title [String, nil] the work's title, cuttered when there is no
64
81
  # author
65
82
  # @param max_len [Integer, nil] digits in the number (default 3, at most 4)
83
+ # @param lang [String, nil] the work's language, for `strip_article`
84
+ # @param strip_article [Boolean, nil] file the title without its initial
85
+ # article in `lang` when truthy (default false)
66
86
  # @return [String] the number, e.g. "C378"
67
- # @raise [ArgumentError] on unusable input
68
- def self.cutter_loc(forename: nil, surname: nil, title: nil, max_len: nil)
69
- Native.cutter_loc(forename, surname, title, max_len)
87
+ # @raise [ArgumentError] on unusable input, an unknown `lang`, or
88
+ # `strip_article` without `lang`
89
+ def self.cutter_loc(forename: nil, surname: nil, title: nil, max_len: nil,
90
+ lang: nil, strip_article: false)
91
+ Native.cutter_loc(forename, surname, title, max_len, lang, strip_article)
70
92
  end
71
93
 
72
94
  # The Library of Congress number's parts.
@@ -75,9 +97,10 @@ module Csan
75
97
  #
76
98
  # @return [Hash{Symbol => String, Integer}] `:alpha_prefix`, `:numeric`,
77
99
  # and `:formatted`
78
- # @raise [ArgumentError] on unusable input
79
- def self.cutter_loc_detailed(forename: nil, surname: nil, title: nil, max_len: nil)
80
- Native.cutter_loc_detailed(forename, surname, title, max_len)
100
+ # @raise [ArgumentError] as {cutter_loc} does
101
+ def self.cutter_loc_detailed(forename: nil, surname: nil, title: nil, max_len: nil,
102
+ lang: nil, strip_article: false)
103
+ Native.cutter_loc_detailed(forename, surname, title, max_len, lang, strip_article)
81
104
  end
82
105
 
83
106
  # Runs the `csan` command-line interface, the engine of the gem's `csan`
data/sig/csan.rbs CHANGED
@@ -17,7 +17,9 @@ module Csan
17
17
  ?surname: String?,
18
18
  ?title: String?,
19
19
  ?prefix_len: Integer?,
20
- ?suffix_len: Integer?
20
+ ?suffix_len: Integer?,
21
+ ?lang: String?,
22
+ ?strip_article: boolish
21
23
  ) -> String
22
24
 
23
25
  def self.cutter_sanborn_detailed: (
@@ -25,33 +27,40 @@ module Csan
25
27
  ?surname: String?,
26
28
  ?title: String?,
27
29
  ?prefix_len: Integer?,
28
- ?suffix_len: Integer?
30
+ ?suffix_len: Integer?,
31
+ ?lang: String?,
32
+ ?strip_article: boolish
29
33
  ) -> sanborn_parts
30
34
 
31
35
  def self.cutter_loc: (
32
36
  ?forename: String?,
33
37
  ?surname: String?,
34
38
  ?title: String?,
35
- ?max_len: Integer?
39
+ ?max_len: Integer?,
40
+ ?lang: String?,
41
+ ?strip_article: boolish
36
42
  ) -> String
37
43
 
38
44
  def self.cutter_loc_detailed: (
39
45
  ?forename: String?,
40
46
  ?surname: String?,
41
47
  ?title: String?,
42
- ?max_len: Integer?
48
+ ?max_len: Integer?,
49
+ ?lang: String?,
50
+ ?strip_article: boolish
43
51
  ) -> loc_parts
44
52
 
45
53
  def self.run_cli: (Array[String] args) -> Integer
46
54
 
47
- # The extension: positional arguments, in the order of the keywords above.
55
+ # The extension, internal to the gem (lib/csan.rb is its only caller):
56
+ # positional arguments, in the order of the keywords above.
48
57
  module Native
49
58
  VERSION: String
50
59
 
51
- def self.cutter_sanborn: (String?, String?, String?, Integer?, Integer?) -> String
52
- def self.cutter_sanborn_detailed: (String?, String?, String?, Integer?, Integer?) -> sanborn_parts
53
- def self.cutter_loc: (String?, String?, String?, Integer?) -> String
54
- def self.cutter_loc_detailed: (String?, String?, String?, Integer?) -> loc_parts
60
+ def self.cutter_sanborn: (String?, String?, String?, Integer?, Integer?, String?, boolish) -> String
61
+ def self.cutter_sanborn_detailed: (String?, String?, String?, Integer?, Integer?, String?, boolish) -> sanborn_parts
62
+ def self.cutter_loc: (String?, String?, String?, Integer?, String?, boolish) -> String
63
+ def self.cutter_loc_detailed: (String?, String?, String?, Integer?, String?, boolish) -> loc_parts
55
64
  def self.run_cli: (Array[String]) -> Integer
56
65
  end
57
66
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - VERALVX