Skip to main content

acorn/schema/pid/
patent.rs

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