Skip to main content

acorn/schema/pid/
patent.rs

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