Skip to main content

acorn/schema/pid/patent/
mod.rs

1//! Data model for patent data
2use super::{PersistentIdentifier, PersistentIdentifierParse};
3use crate::prelude::{format, vec, String, ToString, Vec};
4use crate::util::constants::{RE_PATENT, RE_PATENT_TEXT};
5use crate::util::regex_capture_lookup;
6use core::fmt;
7use derive_more::Display;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11const PATENT_COUNTRY_CODES: [&str; 6] = ["US", "CN", "DE", "EP", "JP", "KR"];
12
13/// Country code for identifying country authority
14#[derive(Clone, Debug, Default, Display, Deserialize, Serialize, JsonSchema)]
15pub enum CountryCode {
16    /// United States
17    #[default]
18    #[display("US")]
19    US,
20    /// Chinese
21    #[display("CN")]
22    CN,
23    /// German
24    #[display("DE")]
25    DE,
26    /// European
27    #[display("EP")]
28    EP,
29    /// Japanese
30    #[display("JP")]
31    JP,
32    /// Korean
33    #[display("KR")]
34    KR,
35}
36/// Patent and Patent-application document kind codes (WIPO ST.16 style).
37///
38/// This enum focuses on currently used codes for USPTO patent-related documents (utility, design, plant, reissue, reexamination, etc.).
39///
40/// ### Notes
41/// - Codes follow WIPO ST.16 and USPTO practice (e.g., "US 7,654,321 B2")
42/// - Trademark-only codes (e.g., X3, X4, X5, X8) are not included
43/// - Some codes are legacy but still appear on older documents (e.g., H, H1)
44#[derive(Clone, Copy, Debug, Deserialize, Display, Eq, Hash, PartialEq, Serialize, JsonSchema)]
45pub enum KindCode {
46    /// Utility patent application, first publication (pre‑grant)
47    #[display("A1")]
48    #[serde(rename = "A1")]
49    A1,
50    /// Second or subsequent publication of a utility patent application
51    #[display("A2")]
52    #[serde(rename = "A2")]
53    A2,
54    /// Utility patent application, corrected publication
55    #[display("A9")]
56    #[serde(rename = "A9")]
57    A9,
58    /// Utility patent grant, no pre‑grant publication (post‑Jan 2, 2001 use)
59    #[display("B1")]
60    #[serde(rename = "B1")]
61    B1,
62    /// Utility patent grant, with pre‑grant publication
63    #[display("B2")]
64    #[serde(rename = "B2")]
65    B2,
66    /// Identifies a reexamination certificate issued after the first reexamination proceeding for a granted US patent (current C‑series replacement for older B1/B2/B3)
67    #[display("C1")]
68    #[serde(rename = "C1")]
69    C1,
70    /// Identifies a reexamination certificate issued after a second reexamination proceeding on the same patent (i.e., the patent has been reexamined twice)
71    #[display("C2")]
72    #[serde(rename = "C2")]
73    C2,
74    /// Identifies a reexamination certificate issued after a third reexamination proceeding on that patent
75    #[display("C3")]
76    #[serde(rename = "C3")]
77    C3,
78    /// Reissue patent (normalized to two‑position form)
79    #[display("E1")]
80    #[serde(rename = "E1", alias = "E")]
81    E1,
82    /// Design patent grant (normalized to two‑position form)
83    #[display("S1")]
84    #[serde(rename = "S1", alias = "S")]
85    S1,
86    /// Plant application publication, first publication
87    #[display("P1")]
88    #[serde(rename = "P1")]
89    P1,
90    /// Plant patent grant, no pre‑grant publication (post‑Jan 2, 2001)
91    #[display("P2")]
92    #[serde(rename = "P2")]
93    P2,
94    /// Plant patent grant, with pre‑grant publication (post‑Jan 2, 2001)
95    #[display("P3")]
96    #[serde(rename = "P3")]
97    P3,
98    /// Second or subsequent publication of a plant patent application
99    #[display("P4")]
100    #[serde(rename = "P4")]
101    P4,
102    /// Corrected publication of a plant patent application
103    #[display("P9")]
104    #[serde(rename = "P9")]
105    P9,
106    /// Statutory invention registration (SIR)
107    /// ### Note
108    /// > SIR program was ended in 2013, so H/H1 appear only on legacy documents.
109    #[display("H1")]
110    #[serde(rename = "H1", alias = "H")]
111    H1,
112    /// Unknown, missing, or otherwise un-supported kind code
113    #[display("Unknown")]
114    #[serde(rename = "Unknown")]
115    Unknown,
116}
117/// Patent and patent-application enumerations for USPTO documents
118#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
119pub enum Patent {
120    /// Granted patent document
121    Granted {
122        /// Country code of patent authority
123        country_code: Option<CountryCode>,
124        /// Kind code
125        kind_code: KindCode,
126        /// 1-7 character serial number
127        serial_number: String,
128    },
129    /// Patent-application document
130    Application {
131        /// 1-7 character serial number
132        serial_number: String,
133        /// Application year (e.g., "2025")
134        year: Option<String>,
135    },
136    /// Patent publication
137    Publication {
138        /// Country code of patent authority
139        country_code: Option<CountryCode>,
140        /// Kind code
141        kind_code: KindCode,
142        /// 1-7 character serial number
143        serial_number: String,
144        /// Publication year (e.g., "2025")
145        year: Option<String>,
146    },
147}
148impl From<&str> for CountryCode {
149    fn from(s: &str) -> Self {
150        match s.to_uppercase().as_str() {
151            | "CN" => CountryCode::CN,
152            | "DE" => CountryCode::DE,
153            | "EP" => CountryCode::EP,
154            | "JP" => CountryCode::JP,
155            | "KR" => CountryCode::KR,
156            | _ => CountryCode::US,
157        }
158    }
159}
160impl From<String> for CountryCode {
161    fn from(s: String) -> Self {
162        CountryCode::from(s.as_str())
163    }
164}
165impl From<&str> for KindCode {
166    fn from(s: &str) -> Self {
167        match s {
168            | "A1" => KindCode::A1,
169            | "A2" => KindCode::A2,
170            | "A9" => KindCode::A9,
171            | "B1" => KindCode::B1,
172            | "B2" => KindCode::B2,
173            | "C1" => KindCode::C1,
174            | "C2" => KindCode::C2,
175            | "C3" => KindCode::C3,
176            | "E" | "E1" => KindCode::E1,
177            | "S" | "S1" => KindCode::S1,
178            | "P1" => KindCode::P1,
179            | "P2" => KindCode::P2,
180            | "P3" => KindCode::P3,
181            | "P4" => KindCode::P4,
182            | "P9" => KindCode::P9,
183            | "H" | "H1" => KindCode::H1,
184            | _ => KindCode::Unknown,
185        }
186    }
187}
188impl From<String> for KindCode {
189    fn from(s: String) -> Self {
190        KindCode::from(s.as_str())
191    }
192}
193impl KindCode {
194    /// Return true if a kind code is a granted patent, false otherwise (e.g., application, publication)
195    pub fn is_granted(&self) -> bool {
196        match self {
197            | KindCode::B1
198            | KindCode::B2
199            | KindCode::C1
200            | KindCode::C2
201            | KindCode::C3
202            | KindCode::E1
203            | KindCode::P2
204            | KindCode::P3
205            | KindCode::S1 => true,
206            | _ => false,
207        }
208    }
209}
210impl Default for Patent {
211    fn default() -> Self {
212        Patent::Granted {
213            country_code: Some(CountryCode::US),
214            kind_code: KindCode::Unknown,
215            serial_number: String::new(),
216        }
217    }
218}
219impl fmt::Display for Patent {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        match self {
222            | Patent::Granted {
223                country_code,
224                serial_number,
225                kind_code,
226            } => {
227                write!(f, "{} {serial_number} {kind_code}", country_code.clone().unwrap_or_default())
228            }
229            | Patent::Application { serial_number, year } => {
230                if let Some(year) = year {
231                    write!(f, "{year}/{serial_number}")
232                } else {
233                    write!(f, "{serial_number}")
234                }
235            }
236            | Patent::Publication {
237                country_code,
238                serial_number,
239                kind_code,
240                year,
241            } => {
242                let country = country_code.clone().unwrap_or_default();
243                if let Some(year) = year {
244                    write!(f, "{country} {year}/{serial_number} {kind_code}")
245                } else {
246                    write!(f, "{country} {serial_number} {kind_code}")
247                }
248            }
249        }
250    }
251}
252impl PersistentIdentifier for Patent {
253    fn new() -> Self {
254        Self::default()
255    }
256    fn schema_uri(&self) -> String {
257        String::new()
258    }
259    fn identifier(&self) -> String {
260        self.to_string()
261    }
262    fn prefix(&self) -> Option<String> {
263        match self {
264            | Self::Granted { country_code, .. } => country_code.as_ref().map(ToString::to_string),
265            | Self::Application { year, .. } => year.clone(),
266            | Self::Publication { country_code, year, .. } => {
267                let prefix = [country_code.as_ref().map(ToString::to_string), year.clone()]
268                    .into_iter()
269                    .flatten()
270                    .collect::<Vec<_>>()
271                    .join("/");
272                (!prefix.is_empty()).then_some(prefix)
273            }
274        }
275    }
276    fn suffix(&self) -> Option<String> {
277        match self {
278            | Self::Application { serial_number, .. } | Self::Granted { serial_number, .. } | Self::Publication { serial_number, .. } => {
279                (!serial_number.is_empty()).then(|| serial_number.clone())
280            }
281        }
282    }
283}
284impl PersistentIdentifierParse for Patent {
285    fn find_all(value: impl ToString) -> Vec<Self> {
286        let value = value.to_string();
287        match Self::parse(&value) {
288            | Some(identifier) => vec![identifier],
289            | None => Self::find_all(&value),
290        }
291    }
292    fn format(value: impl ToString) -> String {
293        Self::from_string(value).to_string()
294    }
295    fn from_string(value: impl ToString) -> Self {
296        Self::from(value.to_string())
297    }
298    fn is_valid(value: impl ToString) -> bool {
299        Self::is_valid(value.to_string())
300    }
301}
302impl From<&str> for Patent {
303    fn from(s: &str) -> Self {
304        Patent::parse(s).unwrap_or_default()
305    }
306}
307impl From<String> for Patent {
308    fn from(s: String) -> Self {
309        Patent::from(s.as_str())
310    }
311}
312impl Patent {
313    /// Find all patent identifier values present in a string
314    pub fn find_all(value: &str) -> Vec<Self> {
315        let re = &RE_PATENT;
316        re.find_iter(value)
317            .filter_map(Result::ok)
318            .filter_map(|m| Patent::parse(m.as_str()))
319            .collect::<Vec<_>>()
320    }
321    /// Check if a string is a valid patent identifier
322    pub fn is_valid<S>(value: S) -> bool
323    where
324        S: AsRef<str>,
325    {
326        match Patent::parse(value.as_ref()) {
327            | Some(result) => match result {
328                | Patent::Application { serial_number, .. } => !serial_number.is_empty(),
329                | Patent::Granted {
330                    serial_number, kind_code, ..
331                }
332                | Patent::Publication {
333                    serial_number, kind_code, ..
334                } => !serial_number.is_empty() && kind_code != KindCode::Unknown,
335            },
336            | None => false,
337        }
338    }
339    /// Parse a patent identifier into a `Patent` enumeration
340    /// ### Note
341    /// > Parsing is focused primarily of patents created after 02/01/2001
342    pub fn parse<S>(value: S) -> Option<Self>
343    where
344        S: Into<String> + Clone,
345    {
346        let s: String = value.clone().into().chars().take(2).collect::<String>();
347        match CountryCode::from(s) {
348            | CountryCode::US => {
349                fn preprocess<S>(value: S) -> String
350                where
351                    S: Into<String>,
352                {
353                    let compact = value.into().replace(" ", "").replace(",", "").replace("-", "").to_uppercase();
354                    let with_kind_code = match compact.chars().last() {
355                        | Some(kind @ ('E' | 'H' | 'S')) => format!("{}{kind}1", compact.trim_end_matches(kind)),
356                        | _ => compact,
357                    };
358                    let is_known_country = PATENT_COUNTRY_CODES.into_iter().any(|country| with_kind_code.starts_with(country));
359                    match is_known_country {
360                        | true => with_kind_code,
361                        | false => format!("US{with_kind_code}"),
362                    }
363                }
364                let pattern = format!("^{RE_PATENT_TEXT}$");
365                let s: String = preprocess(value);
366                let groups = ["country_code", "year", "serial_number", "kind_code"]
367                    .into_iter()
368                    .map(String::from)
369                    .collect::<Vec<_>>();
370                let lookup = regex_capture_lookup(pattern, s, groups);
371                let country_code = lookup.get("country_code").cloned().map(CountryCode::from);
372                let serial_number = lookup.get("serial_number").cloned().unwrap_or_default();
373                let kind_code = match lookup.get("kind_code").cloned() {
374                    | Some(value) => KindCode::from(value),
375                    | None => KindCode::Unknown,
376                };
377                match kind_code {
378                    | KindCode::B1
379                    | KindCode::B2
380                    | KindCode::C1
381                    | KindCode::C2
382                    | KindCode::C3
383                    | KindCode::E1
384                    | KindCode::P2
385                    | KindCode::P3
386                    | KindCode::S1 => Some(Patent::Granted {
387                        country_code,
388                        serial_number,
389                        kind_code,
390                    }),
391                    | KindCode::A1 | KindCode::A2 | KindCode::A9 | KindCode::P1 | KindCode::P4 | KindCode::P9 => {
392                        let year = lookup.get("year").cloned();
393                        Some(Patent::Publication {
394                            country_code,
395                            serial_number,
396                            kind_code,
397                            year,
398                        })
399                    }
400                    | _ => None,
401                }
402            }
403            | _ => None,
404        }
405    }
406}
407
408#[cfg(test)]
409mod tests;