Advertisement
Guest User

Untitled

a guest
Oct 22nd, 2024
16
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Rust 42.66 KB | None | 0 0
  1. use lazy_static::lazy_static;
  2. use regex::Regex;
  3. use scraper::Html;
  4. use serde::{Deserialize, Serialize};
  5. use std::collections::HashSet;
  6.  
  7. use crate::units::{is_commons, is_rights, is_units, is_warrants};
  8.  
  9. // Logic for facts table
  10.  
  11. #[derive(Debug, PartialEq, Deserialize, Serialize)]
  12. pub struct ManagementMember {
  13.     #[doc = "The name of the SPAC member, for example \"John Doe\"."]
  14.     pub name: String,
  15.     #[doc = "Member age, for example 59."]
  16.     pub age: u8,
  17.     #[doc = "Member position, for example \"Chairman, Chief Executive Officer and Director\"."]
  18.     #[serde(skip_serializing_if = "Option::is_none")]
  19.     pub position: Option<String>,
  20. }
  21.  
  22. #[derive(Debug, PartialEq, Deserialize, Serialize)]
  23. pub struct Underwriter {
  24.     #[doc = "Name of the underwriting bank, for example \"Deutsche Bank Securities Inc.\"."]
  25.     pub name: String,
  26.     #[doc = "Amount of units belonging to that bank."]
  27.     #[serde(skip_serializing_if = "Option::is_none")]
  28.     pub units: Option<u64>,
  29. }
  30.  
  31. #[derive(Debug, PartialEq, Deserialize, Serialize)]
  32. pub struct Tickers {
  33.     #[serde(skip_serializing_if = "Option::is_none")]
  34.     pub common: Option<String>,
  35.     #[serde(skip_serializing_if = "Option::is_none")]
  36.     pub units: Option<String>,
  37.     #[serde(skip_serializing_if = "Option::is_none")]
  38.     pub warrants: Option<String>,
  39.     #[serde(skip_serializing_if = "Option::is_none")]
  40.     pub rights: Option<String>,
  41. }
  42.  
  43. #[derive(Debug, PartialEq, Deserialize, Serialize)]
  44. pub enum FinancialType {
  45.     #[serde(rename = "ipo")]
  46.     IPO,
  47.     #[serde(rename = "expense")]
  48.     Expense,
  49. }
  50.  
  51. #[derive(Debug, PartialEq, Deserialize, Serialize)]
  52. pub struct Financial {
  53.     #[doc = "Type of the financial, either ipo or expenses."]
  54.     #[serde(rename = "type")]
  55.     pub value_type: FinancialType,
  56.     #[doc = "Currency symbol."]
  57.     pub currency: String,
  58.     #[doc = "Unit price value."]
  59.     pub per_unit_value: f32,
  60.     #[doc = "Total value = unit price × amount."]
  61.     pub total_value: u64,
  62. }
  63.  
  64. #[derive(Debug, PartialEq, Deserialize, Serialize)]
  65. pub struct S1Info {
  66.     pub tickers: Tickers,
  67.     #[serde(skip_serializing_if = "Option::is_none")]
  68.     pub dilution: Option<String>,
  69.     pub financial: Vec<Financial>,
  70.     pub management: Vec<ManagementMember>,
  71.     pub underwriters: Vec<Underwriter>,
  72. }
  73.  
  74. pub struct S1Parser {
  75.     pub document: Html,
  76. }
  77.  
  78. lazy_static! {
  79.     static ref TICKERS: Regex = Regex::new(r#"[“"](([A-Z]+[. -]?){3,7})[,”"]"#).unwrap();
  80.     static ref TICKER_SYMBOLS: Regex = Regex::new(r#"(under\s+the\s+symbol[s]?)\s*(.*)"#).unwrap();
  81.     static ref IPO: Regex = Regex::new(r"offering|Public").unwrap();
  82.     static ref EXPENSE: Regex = Regex::new(r"(C|c)ommissions").unwrap();
  83.     static ref PRICE: Regex = Regex::new(r"(\$)\s?(\d{1,9})").unwrap();
  84.     static ref PRICE_DOUBLE: Regex = Regex::new(r"(\$)\s?(\d{1,3}\.\d{1,2})").unwrap();
  85.     static ref DOUBLE_DOT: Regex = Regex::new(r"\.{2,}$").unwrap();
  86.     static ref DILUTION: Regex =
  87.         Regex::new(r".*(of approximately)[\s.]([0-9]{1,3}([,|.][0-9]{1,10})?%)").unwrap();
  88.     static ref BANNED_MANAGEMENT_NAMES: Regex =
  89.         Regex::new(r"individual|name|officer|director|founder").unwrap();
  90.     static ref BANNED_UNDERWRITER_NAMES: Regex = Regex::new(
  91.         r"offering|book value|shares|proceeds|expenses|commissions|fees|trust account|denominator|(U|u)nderwriter|miscellaneous|total|\[●\]|\[•\]|\u{200b}"
  92.           ).unwrap();
  93. }
  94.  
  95. /// Helper method to normalize spaces, remove soft hyphens and replace excessive whitespaces
  96. /// with a single whitespace.
  97. fn clear_content(content: &str) -> String {
  98.     content.replace("\u{00ad}", " ").replace("\u{200b}", " ")
  99. }
  100.  
  101. /// Helper method to extract ticker names in quotes, such as “ACEV.U,” or “ACEV”
  102. fn extract_tickers(text: &str) -> HashSet<&str> {
  103.     TICKERS
  104.         .captures_iter(text)
  105.         .map(|mat| mat.get(1).unwrap().as_str())
  106.         .collect()
  107. }
  108.  
  109. impl S1Parser {
  110.     /// Create a new instance and load S-1 filling HTML into memory.
  111.     pub fn new(data: &str) -> Self {
  112.         S1Parser {
  113.             document: Html::parse_document(clear_content(data).as_str()),
  114.         }
  115.     }
  116.  
  117.     /// Get the SPAC market capitalization and expenses.
  118.     pub fn get_market_cap(&self) -> Vec<Financial> {
  119.         let mut results: Vec<Financial> = Vec::new();
  120.  
  121.         let table_selector = scraper::Selector::parse("table").unwrap();
  122.         let mut tables = self.document.select(&table_selector).filter(|item| {
  123.             let text = item.inner_html();
  124.             text.contains("Public offering")
  125.                 || text.contains("Price to Public")
  126.                 || text.contains("Price to public")
  127.         });
  128.  
  129.         if let Some(parent) = tables.next() {
  130.             let tr_selector = scraper::Selector::parse("tr").unwrap();
  131.             let tr = parent.select(&tr_selector).into_iter();
  132.  
  133.             // Select only 1-3 columns
  134.             for (index, cell) in tr.enumerate() {
  135.                 let mut value_type: Option<FinancialType> = None;
  136.                 let mut currency: Option<String> = None;
  137.                 let mut per_unit_value: f32 = 0.0;
  138.                 let mut total_value: u64 = 0;
  139.  
  140.                 if index > 0 && index < 4 {
  141.                     let td_selector = scraper::Selector::parse("td").unwrap();
  142.                     let tds = cell
  143.                         .select(&td_selector)
  144.                         .map(|item| {
  145.                             item.text()
  146.                                 .collect::<String>()
  147.                                 .trim()
  148.                                 .replace(r",", "")
  149.                                 .split_whitespace()
  150.                                 .collect::<Vec<_>>()
  151.                                 .join(" ")
  152.                         })
  153.                         .filter(|item| !item.is_empty() && item.is_ascii())
  154.                         .into_iter();
  155.  
  156.                     for (i, td) in tds.enumerate() {
  157.                         let content = td.as_str();
  158.  
  159.                         // Header
  160.                         if i == 0 {
  161.                             if IPO.is_match(content) {
  162.                                 value_type = Some(FinancialType::IPO);
  163.                             }
  164.                             if EXPENSE.is_match(content) {
  165.                                 value_type = Some(FinancialType::Expense);
  166.                             }
  167.  
  168.                             continue;
  169.                         }
  170.  
  171.                         // Currency sign (per unit)
  172.                         if i == 1 {
  173.                             let captures = PRICE_DOUBLE.captures(content);
  174.  
  175.                             // Check if value is merged with currency sign
  176.                             if let Some(matches) = captures {
  177.                                 per_unit_value = matches
  178.                                     .get(2)
  179.                                     .unwrap()
  180.                                     .as_str()
  181.                                     .parse::<f32>()
  182.                                     .unwrap_or(0.0);
  183.                             }
  184.  
  185.                             continue;
  186.                         }
  187.  
  188.                         // Per single unit
  189.                         if i == 2 && per_unit_value == 0.0 {
  190.                             per_unit_value = content.parse::<f32>().unwrap_or(0.0);
  191.  
  192.                             continue;
  193.                         } else {
  194.                             let captures = PRICE.captures(content);
  195.  
  196.                             // Check if value is merged with currency sign
  197.                             if let Some(matches) = captures {
  198.                                 currency = Some(matches.get(1).unwrap().as_str().to_string());
  199.                                 let value = matches.get(2).unwrap().as_str();
  200.                                 total_value = value.parse::<u64>().unwrap_or(0);
  201.                                 continue;
  202.                             }
  203.                         }
  204.  
  205.                         // Currency sign (total)
  206.                         if i == 3 && currency.is_none() {
  207.                             currency = Some(content.to_string());
  208.                             continue;
  209.                         }
  210.  
  211.                         // Total value
  212.                         if i == 4 && total_value == 0 {
  213.                             total_value = content.parse::<u64>().unwrap_or(0);
  214.                             continue;
  215.                         }
  216.                     }
  217.                 }
  218.  
  219.                 if !value_type.is_none() && total_value > 10 && per_unit_value > 0.0 {
  220.                     results.insert(
  221.                         0,
  222.                         Financial {
  223.                             value_type: value_type.unwrap(),
  224.                             currency: currency.unwrap_or("$".to_owned()),
  225.                             per_unit_value,
  226.                             total_value,
  227.                         },
  228.                     );
  229.                 }
  230.             }
  231.         }
  232.  
  233.         results
  234.     }
  235.  
  236.     /// Parse the dilution to public shareholders percentage.
  237.     pub fn get_dilution(&self) -> Option<String> {
  238.         let content = self.document.root_element().text().collect::<String>();
  239.         let captures = DILUTION.captures(content.as_str());
  240.  
  241.         // We have a match
  242.         if let Some(matches) = captures {
  243.             return Some(matches.get(2).unwrap().as_str().to_string());
  244.         }
  245.  
  246.         None
  247.     }
  248.  
  249.     /// Parse the management team table, return a vector of members (age, name and position).
  250.     pub fn get_management_members(&self) -> Vec<ManagementMember> {
  251.         let mut results: Vec<ManagementMember> = Vec::new();
  252.  
  253.         let table_selector = scraper::Selector::parse("table").unwrap();
  254.         // Get header row
  255.         let mut tables = self
  256.             .document
  257.             .select(&table_selector)
  258.             .filter(|item| item.inner_html().contains("Age") && item.inner_html().contains("Name"));
  259.  
  260.         if let Some(parent) = tables.next() {
  261.             let tr_selector = scraper::Selector::parse("tr").unwrap();
  262.             let tr = parent.select(&tr_selector).into_iter();
  263.  
  264.             for (index, cell) in tr.enumerate() {
  265.                 let mut name: Option<String> = None;
  266.                 let mut age: u8 = 0;
  267.                 let mut position: Option<String> = None;
  268.  
  269.                 // Skip the very first, header row
  270.                 if index != 0 {
  271.                     let td_selector = scraper::Selector::parse("td").unwrap();
  272.                     let tds = cell
  273.                         .select(&td_selector)
  274.                         .map(|item| {
  275.                             item.text()
  276.                                 .collect::<String>()
  277.                                 .trim()
  278.                                 .split_whitespace()
  279.                                 .collect::<Vec<_>>()
  280.                                 .join(" ")
  281.                         })
  282.                         .filter(|item| item.is_ascii() && !item.is_empty())
  283.                         .into_iter();
  284.  
  285.                     // Iterate through each cell
  286.                     for (i, td) in tds.enumerate() {
  287.                         let content = td.as_str();
  288.  
  289.                         // Check the cell number
  290.                         match i {
  291.                             // Cell contains name value
  292.                             0 => {
  293.                                 let content = DOUBLE_DOT.replace_all(&content, "");
  294.  
  295.                                 if !BANNED_MANAGEMENT_NAMES.is_match(content.trim()) {
  296.                                     name = Some(content.to_string());
  297.                                 } else {
  298.                                     name = None;
  299.                                 }
  300.                             }
  301.                             // Cell contains age
  302.                             1 => {
  303.                                 age = content.parse::<u8>().unwrap_or(0);
  304.                             }
  305.                             // Cell contains position
  306.                             2 => {
  307.                                 position = Some(content.to_string());
  308.                             }
  309.                             // Skip all other cells
  310.                             _ => (),
  311.                         }
  312.                     }
  313.                 }
  314.  
  315.                 if !name.is_none() && age > 0 {
  316.                     results.insert(
  317.                         0,
  318.                         ManagementMember {
  319.                             name: name.unwrap(),
  320.                             age,
  321.                             position,
  322.                         },
  323.                     );
  324.                 }
  325.             }
  326.         }
  327.  
  328.         results
  329.     }
  330.  
  331.     /// Parse the table of underwriters from S1 filling and return an array.
  332.     pub fn get_underwriters(&self) -> Vec<Underwriter> {
  333.         let mut results: Vec<Underwriter> = Vec::new();
  334.  
  335.         let table_selector = scraper::Selector::parse("table").unwrap();
  336.         // Get header row
  337.         let mut tables = self
  338.             .document
  339.             .select(&table_selector)
  340.             .filter(|item| item.inner_html().contains("Underwriter"));
  341.  
  342.         if let Some(parent) = tables.next() {
  343.             let tr_selector = scraper::Selector::parse("tr").unwrap();
  344.             let tr = parent.select(&tr_selector).into_iter();
  345.  
  346.             for (index, cell) in tr.enumerate() {
  347.                 let mut name: Option<String> = None;
  348.                 let mut units: Option<u64> = None;
  349.  
  350.                 // Skip the very first, header row
  351.                 if index != 0 {
  352.                     let td_selector = scraper::Selector::parse("td").unwrap();
  353.                     let tds = cell
  354.                         .select(&td_selector)
  355.                         .map(|item| {
  356.                             // Remove excessive whitespaces
  357.                             item.text()
  358.                                 .collect::<String>()
  359.                                 .trim()
  360.                                 .split_whitespace()
  361.                                 .collect::<Vec<_>>()
  362.                                 .join(" ")
  363.                         })
  364.                         .filter(|item| !item.is_empty())
  365.                         .into_iter();
  366.  
  367.                     // Iterate through each cell
  368.                     for (i, td) in tds.enumerate() {
  369.                         let content = td.as_str();
  370.  
  371.                         // Cell contains name value
  372.                         if i == 0 {
  373.                             // An array of banned words, exclude them from end results
  374.                             if !BANNED_UNDERWRITER_NAMES.is_match(content.trim()) {
  375.                                 name = Some(content.to_string());
  376.                             } else {
  377.                                 name = None;
  378.                             }
  379.                         }
  380.  
  381.                         // Cell contains age
  382.                         if i == 1 {
  383.                             let parsed_units = content.parse::<u64>().unwrap_or(0);
  384.                             if parsed_units == 0 {
  385.                                 units = None;
  386.                             } else {
  387.                                 units = Some(parsed_units);
  388.                             }
  389.                         }
  390.                     }
  391.                 }
  392.  
  393.                 if !name.is_none() && !(name == Some("Total".to_string()) && units.is_none()) {
  394.                     results.insert(
  395.                         0,
  396.                         Underwriter {
  397.                             name: name.unwrap(),
  398.                             units,
  399.                         },
  400.                     );
  401.                 }
  402.             }
  403.         }
  404.  
  405.         results
  406.     }
  407.  
  408.     /// Extract common stock, unit, warrant and right tickers from S-1 filling.
  409.     pub fn get_ticker_symbols(&self) -> Tickers {
  410.         let mut units: Option<String> = None;
  411.         let mut warrants: Option<String> = None;
  412.         let mut rights: Option<String> = None;
  413.         let mut common: Option<String> = None;
  414.  
  415.         let content = self.document.root_element().text().collect::<String>();
  416.  
  417.         // Find all the paragraphs "under the symbol(s)"
  418.         for m in TICKER_SYMBOLS.captures_iter(&content) {
  419.             let tickers = extract_tickers(m.get(0).unwrap().as_str());
  420.             for t in tickers {
  421.                 // Replace warrant symbol "WS" with just "W" at the end. This is an exception.
  422.                 // Only happens in rare cases.
  423.                 // Also trim ticker and remove spaces/dots.
  424.                 let ticker = t
  425.                     .trim()
  426.                     .replace(r" WS", "W")
  427.                     .replace(".", "")
  428.                     .replace(" ", "");
  429.  
  430.                 if is_units(&ticker) && units.is_none() {
  431.                     units = Some(ticker);
  432.                     continue;
  433.                 }
  434.  
  435.                 if is_warrants(&ticker) && warrants.is_none() {
  436.                     warrants = Some(ticker);
  437.                     continue;
  438.                 }
  439.  
  440.                 if is_rights(&ticker) && rights.is_none() {
  441.                     rights = Some(ticker);
  442.                     continue;
  443.                 }
  444.  
  445.                 if is_commons(&ticker) && common.is_none() && &ticker != "SEC" {
  446.                     common = Some(ticker);
  447.                     continue;
  448.                 }
  449.             }
  450.         }
  451.  
  452.         return Tickers {
  453.             units,
  454.             warrants,
  455.             rights,
  456.             common,
  457.         };
  458.     }
  459.  
  460.     /// Get all scraped data (dilution, market cap, underwriters, management, tickers).
  461.     pub fn get_all(&self) -> S1Info {
  462.         return S1Info {
  463.             dilution: self.get_dilution(),
  464.             tickers: self.get_ticker_symbols(),
  465.             financial: self.get_market_cap(),
  466.             management: self.get_management_members(),
  467.             underwriters: self.get_underwriters(),
  468.         };
  469.     }
  470. }
  471.  
  472. #[cfg(test)]
  473. mod tests {
  474.     // Note this useful idiom: importing names from outer (for mod tests) scope.
  475.     use super::*;
  476.     use scraper::Selector;
  477.     use std::fs;
  478.  
  479.     struct Setup {
  480.         parser_pacx: S1Parser,
  481.         parser_dkdca: S1Parser,
  482.         parser_eggf: S1Parser,
  483.         parser_bpac: S1Parser,
  484.         parser_aftr: S1Parser,
  485.     }
  486.  
  487.     impl Setup {
  488.         fn new() -> Self {
  489.             let pacx = fs::read_to_string("fixtures/pacx.htm").unwrap();
  490.             let dkdca = fs::read_to_string("fixtures/dkdca.htm").unwrap();
  491.             let eggf = fs::read_to_string("fixtures/eggf.htm").unwrap();
  492.             let bpac = fs::read_to_string("fixtures/bpac.htm").unwrap();
  493.             let aftr = fs::read_to_string("fixtures/aftr.htm").unwrap();
  494.             let parser_pacx = S1Parser::new(&pacx);
  495.             let parser_dkdca = S1Parser::new(&dkdca);
  496.             let parser_eggf = S1Parser::new(&eggf);
  497.             let parser_bpac = S1Parser::new(&bpac);
  498.             let parser_aftr = S1Parser::new(&aftr);
  499.  
  500.             Self {
  501.                 parser_pacx,
  502.                 parser_dkdca,
  503.                 parser_eggf,
  504.                 parser_bpac,
  505.                 parser_aftr,
  506.             }
  507.         }
  508.     }
  509.  
  510.     #[test]
  511.     fn ut_s1parser_clear_content() {
  512.         let bad_content = "Test\u{00ad}\u{00ad}\u{00ad}\u{00ad}\u{200b} here is some text";
  513.         let actual = clear_content(bad_content);
  514.         assert_eq!(actual, "Test      here is some text")
  515.     }
  516.  
  517.     #[test]
  518.     fn ut_s1parser_new() {
  519.         // Must instantiate S1 Parser
  520.         let html = r#"
  521.      <ul>
  522.        <li>Foo</li>
  523.        <li>Bar</li>
  524.        <li>Baz</li>
  525.      </ul>
  526.    "#;
  527.         let fragment = S1Parser::new(html);
  528.         let selector = Selector::parse("ul > li:nth-child(2)").unwrap();
  529.         let li = fragment.document.select(&selector).next().unwrap();
  530.  
  531.         assert_eq!("<li>Bar</li>", li.html());
  532.     }
  533.  
  534.     #[test]
  535.     fn ut_s1parser_get_market_cap() {
  536.         // Try to get market cap
  537.         let setup = Setup::new();
  538.         let actual_pacx = setup.parser_pacx.get_market_cap();
  539.         let actual_dkdca = setup.parser_dkdca.get_market_cap();
  540.         let actual_eggf = setup.parser_eggf.get_market_cap();
  541.         let actual_bpac = setup.parser_bpac.get_market_cap();
  542.         let actual_aftr = setup.parser_aftr.get_market_cap();
  543.  
  544.         assert_eq!(
  545.             actual_pacx,
  546.             vec![
  547.                 Financial {
  548.                     value_type: FinancialType::Expense,
  549.                     currency: "$".to_owned(),
  550.                     per_unit_value: 0.55,
  551.                     total_value: 19250000,
  552.                 },
  553.                 Financial {
  554.                     value_type: FinancialType::IPO,
  555.                     currency: "$".to_owned(),
  556.                     per_unit_value: 10.00,
  557.                     total_value: 350000000,
  558.                 }
  559.             ]
  560.         );
  561.  
  562.         assert_eq!(
  563.             actual_dkdca,
  564.             vec![
  565.                 Financial {
  566.                     value_type: FinancialType::Expense,
  567.                     currency: "$".to_owned(),
  568.                     per_unit_value: 0.55,
  569.                     total_value: 5500000,
  570.                 },
  571.                 Financial {
  572.                     value_type: FinancialType::IPO,
  573.                     currency: "$".to_owned(),
  574.                     per_unit_value: 10.00,
  575.                     total_value: 100000000,
  576.                 }
  577.             ]
  578.         );
  579.  
  580.         assert_eq!(
  581.             actual_eggf,
  582.             vec![
  583.                 Financial {
  584.                     value_type: FinancialType::Expense,
  585.                     currency: "$".to_owned(),
  586.                     per_unit_value: 0.55,
  587.                     total_value: 13750000,
  588.                 },
  589.                 Financial {
  590.                     value_type: FinancialType::IPO,
  591.                     currency: "$".to_owned(),
  592.                     per_unit_value: 10.00,
  593.                     total_value: 250000000,
  594.                 }
  595.             ]
  596.         );
  597.  
  598.         assert_eq!(
  599.             actual_bpac,
  600.             vec![
  601.                 Financial {
  602.                     value_type: FinancialType::Expense,
  603.                     currency: "$".to_owned(),
  604.                     per_unit_value: 0.55,
  605.                     total_value: 11000000,
  606.                 },
  607.                 Financial {
  608.                     value_type: FinancialType::IPO,
  609.                     currency: "$".to_owned(),
  610.                     per_unit_value: 10.00,
  611.                     total_value: 200000000,
  612.                 }
  613.             ]
  614.         );
  615.  
  616.         assert_eq!(
  617.             actual_aftr,
  618.             vec![
  619.                 Financial {
  620.                     value_type: FinancialType::Expense,
  621.                     currency: "$".to_owned(),
  622.                     per_unit_value: 0.55,
  623.                     total_value: 16500000,
  624.                 },
  625.                 Financial {
  626.                     value_type: FinancialType::IPO,
  627.                     currency: "$".to_owned(),
  628.                     per_unit_value: 10.00,
  629.                     total_value: 300000000,
  630.                 }
  631.             ]
  632.         );
  633.     }
  634.  
  635.     #[test]
  636.     fn ut_s1parser_get_dilution() {
  637.         let setup = Setup::new();
  638.         let actual_pacx = setup.parser_pacx.get_dilution();
  639.         let actual_dkdca = setup.parser_dkdca.get_dilution();
  640.         let actual_eggf = setup.parser_eggf.get_dilution();
  641.         let actual_bpac = setup.parser_bpac.get_dilution();
  642.         let actual_aftr = setup.parser_aftr.get_dilution();
  643.         assert_eq!(actual_pacx, Some(String::from("95.2%")));
  644.         assert_eq!(actual_dkdca, Some(String::from("88.4%")));
  645.         assert_eq!(actual_eggf, Some(String::from("93.4%")));
  646.         assert_eq!(actual_bpac, Some(String::from("135.5%")));
  647.         assert_eq!(actual_aftr, None);
  648.     }
  649.  
  650.     #[test]
  651.     fn ut_s1parser_get_dilution_none() {
  652.         let html = r#"
  653.      <div style="color: #000000; font-family: 'Times New Roman', Times, serif; font-size: 10pt; text-align: justify;">
  654.        Our letter agreement with our initial shareholders, officers and directors contain provisions relating to transfer restrictions of our founder shares and private placement warrants, indemnification of the trust account, waiver of redemption rights and participation in liquidating distributions from the trust account. The letter agreement and the registration rights agreement may be amended, and provisions therein may be waived, without shareholder approval (although releasing the parties from the restriction contained in the letter agreement
  655.      </div>
  656.    "#;
  657.         let parser = S1Parser::new(html);
  658.         let dilution = parser.get_dilution();
  659.         assert_eq!(dilution, None);
  660.     }
  661.  
  662.     #[test]
  663.     fn ut_s1parser_get_management_members() {
  664.         let setup = Setup::new();
  665.         let actual_pacx = setup.parser_pacx.get_management_members();
  666.         let actual_dkdca = setup.parser_dkdca.get_management_members();
  667.         let actual_eggf = setup.parser_eggf.get_management_members();
  668.         let actual_bpac = setup.parser_bpac.get_management_members();
  669.         let actual_aftr = setup.parser_aftr.get_management_members();
  670.  
  671.         assert_eq!(
  672.             actual_pacx,
  673.             vec![
  674.                 ManagementMember {
  675.                     name: "Todd Davis".to_owned(),
  676.                     age: 52,
  677.                     position: Some("Director Nominee".to_owned())
  678.                 },
  679.                 ManagementMember {
  680.                     name: "Mitchell Caplan".to_owned(),
  681.                     age: 63,
  682.                     position: Some("Director Nominee".to_owned())
  683.                 },
  684.                 ManagementMember {
  685.                     name: "Matthew Corey".to_owned(),
  686.                     age: 36,
  687.                     position: Some("Chief Financial Officer".to_owned())
  688.                 },
  689.                 ManagementMember {
  690.                     name: "Scott Carpenter".to_owned(),
  691.                     age: 49,
  692.                     position: Some("Chief Operating Officer".to_owned())
  693.                 },
  694.                 ManagementMember {
  695.                     name: "Ryan Khoury".to_owned(),
  696.                     age: 37,
  697.                     position: Some("Chief Executive Officer".to_owned())
  698.                 },
  699.                 ManagementMember {
  700.                     name: "Oscar Salazar".to_owned(),
  701.                     age: 43,
  702.                     position: Some("Co-President and Director Nominee".to_owned())
  703.                 },
  704.                 ManagementMember {
  705.                     name: "Rick Gerson".to_owned(),
  706.                     age: 45,
  707.                     position: Some("Co-President".to_owned())
  708.                 },
  709.                 ManagementMember {
  710.                     name: "Jonathan Christodoro".to_owned(),
  711.                     age: 44,
  712.                     position: Some("Chairman".to_owned())
  713.                 }
  714.             ]
  715.         );
  716.  
  717.         assert_eq!(
  718.             actual_dkdca,
  719.             vec![
  720.                 ManagementMember {
  721.                     name: "Julianne Huh".to_owned(),
  722.                     age: 52,
  723.                     position: Some("Director nominee".to_owned())
  724.                 },
  725.                 ManagementMember {
  726.                     name: "Syed Musheer Ahmed".to_owned(),
  727.                     age: 37,
  728.                     position: Some("Director nominee".to_owned())
  729.                 },
  730.                 ManagementMember {
  731.                     name: "Firdauz Edmin Bin Mokhtar".to_owned(),
  732.                     age: 48,
  733.                     position: Some("Chief Financial Officer and Secretary".to_owned())
  734.                 },
  735.                 ManagementMember {
  736.                     name: "Barry Anderson".to_owned(),
  737.                     age: 44,
  738.                     position: Some("Chairman, Chief Executive Officer and Director".to_owned())
  739.                 }
  740.             ]
  741.         );
  742.  
  743.         assert_eq!(
  744.             actual_eggf,
  745.             vec![
  746.                 ManagementMember {
  747.                     name: "Noorsurainah (Su) Tengah".to_owned(),
  748.                     age: 38,
  749.                     position: Some("Director Nominee".to_owned())
  750.                 },
  751.                 ManagementMember {
  752.                     name: "Jonathan Silver".to_owned(),
  753.                     age: 63,
  754.                     position: Some("Director Nominee".to_owned())
  755.                 },
  756.                 ManagementMember {
  757.                     name: "Linda Hall Daschle".to_owned(),
  758.                     age: 65,
  759.                     position: Some("Director Nominee".to_owned())
  760.                 },
  761.                 ManagementMember {
  762.                     name: "Louise Curbishley".to_owned(),
  763.                     age: 47,
  764.                     position: Some("Director Nominee".to_owned())
  765.                 },
  766.                 ManagementMember {
  767.                     name: "Sophia Park Mullen".to_owned(),
  768.                     age: 42,
  769.                     position: Some("President and Director".to_owned())
  770.                 },
  771.                 ManagementMember {
  772.                     name: "Gary Fegel".to_owned(),
  773.                     age: 47,
  774.                     position: Some("Chairman".to_owned())
  775.                 },
  776.                 ManagementMember {
  777.                     name: "Gregg S. Hymowitz".to_owned(),
  778.                     age: 55,
  779.                     position: Some("Chief Executive Officer and Director".to_owned())
  780.                 }
  781.             ]
  782.         );
  783.  
  784.         assert_eq!(
  785.             actual_bpac,
  786.             vec![
  787.                 ManagementMember {
  788.                     name: "Les Ottolenghi".to_owned(),
  789.                     age: 59,
  790.                     position: Some("Director".to_owned())
  791.                 },
  792.                 ManagementMember {
  793.                     name: "Brett Calapp".to_owned(),
  794.                     age: 46,
  795.                     position: Some("Director".to_owned())
  796.                 },
  797.                 ManagementMember {
  798.                     name: "Melissa Blau".to_owned(),
  799.                     age: 52,
  800.                     position: Some("Director".to_owned())
  801.                 },
  802.                 ManagementMember {
  803.                     name: "Duncan Davidson".to_owned(),
  804.                     age: 68,
  805.                     position: Some("Executive Vice President".to_owned())
  806.                 },
  807.                 ManagementMember {
  808.                     name: "Eric Wiesen".to_owned(),
  809.                     age: 46,
  810.                     position: Some("President".to_owned())
  811.                 },
  812.                 ManagementMember {
  813.                     name: "David VanEgmond".to_owned(),
  814.                     age: 32,
  815.                     position: Some("Chief Executive Officer".to_owned())
  816.                 },
  817.                 ManagementMember {
  818.                     name: "Paul Martino".to_owned(),
  819.                     age: 46,
  820.                     position: Some("Executive Chairman".to_owned())
  821.                 }
  822.             ]
  823.         );
  824.  
  825.         assert_eq!(
  826.             actual_aftr,
  827.             vec![
  828.                 ManagementMember {
  829.                     name: "Bharat Sundaram".to_owned(),
  830.                     age: 43,
  831.                     position: Some("Director Nominee".to_owned())
  832.                 },
  833.                 ManagementMember {
  834.                     name: "Bill Miller".to_owned(),
  835.                     age: 54,
  836.                     position: Some("Director Nominee".to_owned())
  837.                 },
  838.                 ManagementMember {
  839.                     name: "Christopher H. Hunter".to_owned(),
  840.                     age: 52,
  841.                     position: Some("Director Nominee".to_owned())
  842.                 },
  843.                 ManagementMember {
  844.                     name: "Dr. Julie Gerberding".to_owned(),
  845.                     age: 65,
  846.                     position: Some("Director Nominee".to_owned())
  847.                 },
  848.                 ManagementMember {
  849.                     name: "A.G. Breitenstein".to_owned(),
  850.                     age: 52,
  851.                     position: Some("Director Nominee".to_owned())
  852.                 },
  853.                 ManagementMember {
  854.                     name: "Nehal Raj".to_owned(),
  855.                     age: 42,
  856.                     position: Some("Director".to_owned())
  857.                 },
  858.                 ManagementMember {
  859.                     name: "Jeffrey Rhodes".to_owned(),
  860.                     age: 46,
  861.                     position: Some("Director".to_owned())
  862.                 },
  863.                 ManagementMember {
  864.                     name: "Martin Davidson".to_owned(),
  865.                     age: 45,
  866.                     position: Some("Chief Financial Officer".to_owned())
  867.                 },
  868.                 ManagementMember {
  869.                     name: "Anthony Colaluca".to_owned(),
  870.                     age: 54,
  871.                     position: Some("President and Director".to_owned())
  872.                 },
  873.                 ManagementMember {
  874.                     name: "R. Halsey Wise".to_owned(),
  875.                     age: 56,
  876.                     position: Some("Chief Executive Officer and Chairman".to_owned())
  877.                 }
  878.             ]
  879.         )
  880.     }
  881.  
  882.     #[test]
  883.     fn ut_s1parser_get_underwriters() {
  884.         let setup = Setup::new();
  885.         let actual_pacx = setup.parser_pacx.get_underwriters();
  886.         let actual_dkdca = setup.parser_dkdca.get_underwriters();
  887.         let actual_eggf = setup.parser_eggf.get_underwriters();
  888.         let actual_bpac = setup.parser_bpac.get_underwriters();
  889.         let actual_aftr = setup.parser_aftr.get_underwriters();
  890.  
  891.         assert_eq!(
  892.             actual_pacx,
  893.             vec![Underwriter {
  894.                 name: "Citigroup Global Markets Inc.".to_owned(),
  895.                 units: None
  896.             }]
  897.         );
  898.  
  899.         assert_eq!(
  900.             actual_dkdca,
  901.             vec![Underwriter {
  902.                 name: "Kingswood Capital Markets, division of Benchmark Investments, Inc."
  903.                     .to_owned(),
  904.                 units: None
  905.             }]
  906.         );
  907.  
  908.         assert_eq!(
  909.             actual_eggf,
  910.             vec![Underwriter {
  911.                 name: "BTIG, LLC".to_owned(),
  912.                 units: None
  913.             }]
  914.         );
  915.  
  916.         assert_eq!(
  917.             actual_bpac,
  918.             vec![Underwriter {
  919.                 name: "Citigroup Global Markets Inc.".to_owned(),
  920.                 units: None
  921.             }]
  922.         );
  923.  
  924.         assert_eq!(
  925.             actual_aftr,
  926.             vec![
  927.                 Underwriter {
  928.                     name: "BofA Securities, Inc.".to_owned(),
  929.                     units: None
  930.                 },
  931.                 Underwriter {
  932.                     name: "Deutsche Bank Securities Inc.".to_owned(),
  933.                     units: None
  934.                 },
  935.                 Underwriter {
  936.                     name: "Goldman Sachs & Co. LLC.".to_owned(),
  937.                     units: None
  938.                 }
  939.             ]
  940.         );
  941.     }
  942.  
  943.     #[test]
  944.     fn ut_extract_tickers() {
  945.         let text = r"A ordinary shares and warrants on Nasdaq under the symbols “ACEV.U,” “ACEV” and “ACEV WS,” respectively.";
  946.         let tickers = extract_tickers(text);
  947.         assert!(
  948.             tickers.contains("ACEV") && tickers.contains("ACEV.U") && tickers.contains("ACEV WS")
  949.         );
  950.         assert_eq!(tickers.len(), 3);
  951.     }
  952.  
  953.     #[test]
  954.     fn ut_s1parser_get_ticker_symbols() {
  955.         let setup = Setup::new();
  956.         let actual_pacx = setup.parser_pacx.get_ticker_symbols();
  957.         let actual_dkdca = setup.parser_dkdca.get_ticker_symbols();
  958.         let actual_eggf = setup.parser_eggf.get_ticker_symbols();
  959.         let actual_bpac = setup.parser_bpac.get_ticker_symbols();
  960.         let actual_aftr = setup.parser_aftr.get_ticker_symbols();
  961.  
  962.         assert_eq!(
  963.             actual_pacx,
  964.             Tickers {
  965.                 common: Some("PACX".to_owned()),
  966.                 units: Some("PACXU".to_owned()),
  967.                 warrants: Some("PACXW".to_owned()),
  968.                 rights: None
  969.             }
  970.         );
  971.  
  972.         assert_eq!(
  973.             actual_dkdca,
  974.             Tickers {
  975.                 common: Some("DKDCA".to_owned()),
  976.                 units: Some("DKDCU".to_owned()),
  977.                 warrants: Some("DKDCW".to_owned()),
  978.                 rights: Some("DKDCR".to_owned())
  979.             }
  980.         );
  981.  
  982.         assert_eq!(
  983.             actual_eggf,
  984.             Tickers {
  985.                 common: Some("EGGF".to_owned()),
  986.                 units: Some("EGGFU".to_owned()),
  987.                 warrants: Some("EGGFW".to_owned()),
  988.                 rights: None
  989.             }
  990.         );
  991.  
  992.         assert_eq!(
  993.             actual_bpac,
  994.             Tickers {
  995.                 common: Some("BPAC".to_owned()),
  996.                 units: Some("BPACU".to_owned()),
  997.                 warrants: Some("BPACW".to_owned()),
  998.                 rights: None
  999.             }
  1000.         );
  1001.  
  1002.         assert_eq!(
  1003.             actual_aftr,
  1004.             Tickers {
  1005.                 common: Some("AFTR".to_owned()),
  1006.                 units: Some("AFTRU".to_owned()),
  1007.                 warrants: Some("AFTRW".to_owned()),
  1008.                 rights: None
  1009.             }
  1010.         );
  1011.     }
  1012.  
  1013.     #[test]
  1014.     fn ut_s1parser_get_all() {
  1015.         let setup = Setup::new();
  1016.         let actual_pacx = setup.parser_pacx.get_all();
  1017.         let actual_dkdca = setup.parser_dkdca.get_all();
  1018.         let actual_eggf = setup.parser_eggf.get_all();
  1019.         let actual_bpac = setup.parser_bpac.get_all();
  1020.         let actual_aftr = setup.parser_aftr.get_all();
  1021.  
  1022.         // Serialize it to a JSON string.
  1023.         let pacx_all_json = serde_json::to_string(&actual_pacx).unwrap();
  1024.         let dkdca_all_json = serde_json::to_string(&actual_dkdca).unwrap();
  1025.         let eggf_all_json = serde_json::to_string(&actual_eggf).unwrap();
  1026.         let bpac_all_json = serde_json::to_string(&actual_bpac).unwrap();
  1027.         let aftr_all_json = serde_json::to_string(&actual_aftr).unwrap();
  1028.  
  1029.         assert_eq!(pacx_all_json, "{\"tickers\":{\"common\":\"PACX\",\"units\":\"PACXU\",\"warrants\":\"PACXW\"},\"dilution\":\"95.2%\",\"financial\":[{\"type\":\"expense\",\"currency\":\"$\",\"per_unit_value\":0.55,\"total_value\":19250000},{\"type\":\"ipo\",\"currency\":\"$\",\"per_unit_value\":10.0,\"total_value\":350000000}],\"management\":[{\"name\":\"Todd Davis\",\"age\":52,\"position\":\"Director Nominee\"},{\"name\":\"Mitchell Caplan\",\"age\":63,\"position\":\"Director Nominee\"},{\"name\":\"Matthew Corey\",\"age\":36,\"position\":\"Chief Financial Officer\"},{\"name\":\"Scott Carpenter\",\"age\":49,\"position\":\"Chief Operating Officer\"},{\"name\":\"Ryan Khoury\",\"age\":37,\"position\":\"Chief Executive Officer\"},{\"name\":\"Oscar Salazar\",\"age\":43,\"position\":\"Co-President and Director Nominee\"},{\"name\":\"Rick Gerson\",\"age\":45,\"position\":\"Co-President\"},{\"name\":\"Jonathan Christodoro\",\"age\":44,\"position\":\"Chairman\"}],\"underwriters\":[{\"name\":\"Citigroup Global Markets Inc.\"}]}");
  1030.         assert_eq!(dkdca_all_json, "{\"tickers\":{\"common\":\"DKDCA\",\"units\":\"DKDCU\",\"warrants\":\"DKDCW\",\"rights\":\"DKDCR\"},\"dilution\":\"88.4%\",\"financial\":[{\"type\":\"expense\",\"currency\":\"$\",\"per_unit_value\":0.55,\"total_value\":5500000},{\"type\":\"ipo\",\"currency\":\"$\",\"per_unit_value\":10.0,\"total_value\":100000000}],\"management\":[{\"name\":\"Julianne Huh\",\"age\":52,\"position\":\"Director nominee\"},{\"name\":\"Syed Musheer Ahmed\",\"age\":37,\"position\":\"Director nominee\"},{\"name\":\"Firdauz Edmin Bin Mokhtar\",\"age\":48,\"position\":\"Chief Financial Officer and Secretary\"},{\"name\":\"Barry Anderson\",\"age\":44,\"position\":\"Chairman, Chief Executive Officer and Director\"}],\"underwriters\":[{\"name\":\"Kingswood Capital Markets, division of Benchmark Investments, Inc.\"}]}");
  1031.         assert_eq!(eggf_all_json, "{\"tickers\":{\"common\":\"EGGF\",\"units\":\"EGGFU\",\"warrants\":\"EGGFW\"},\"dilution\":\"93.4%\",\"financial\":[{\"type\":\"expense\",\"currency\":\"$\",\"per_unit_value\":0.55,\"total_value\":13750000},{\"type\":\"ipo\",\"currency\":\"$\",\"per_unit_value\":10.0,\"total_value\":250000000}],\"management\":[{\"name\":\"Noorsurainah (Su) Tengah\",\"age\":38,\"position\":\"Director Nominee\"},{\"name\":\"Jonathan Silver\",\"age\":63,\"position\":\"Director Nominee\"},{\"name\":\"Linda Hall Daschle\",\"age\":65,\"position\":\"Director Nominee\"},{\"name\":\"Louise Curbishley\",\"age\":47,\"position\":\"Director Nominee\"},{\"name\":\"Sophia Park Mullen\",\"age\":42,\"position\":\"President and Director\"},{\"name\":\"Gary Fegel\",\"age\":47,\"position\":\"Chairman\"},{\"name\":\"Gregg S. Hymowitz\",\"age\":55,\"position\":\"Chief Executive Officer and Director\"}],\"underwriters\":[{\"name\":\"BTIG, LLC\"}]}");
  1032.         assert_eq!(bpac_all_json, "{\"tickers\":{\"common\":\"BPAC\",\"units\":\"BPACU\",\"warrants\":\"BPACW\"},\"dilution\":\"135.5%\",\"financial\":[{\"type\":\"expense\",\"currency\":\"$\",\"per_unit_value\":0.55,\"total_value\":11000000},{\"type\":\"ipo\",\"currency\":\"$\",\"per_unit_value\":10.0,\"total_value\":200000000}],\"management\":[{\"name\":\"Les Ottolenghi\",\"age\":59,\"position\":\"Director\"},{\"name\":\"Brett Calapp\",\"age\":46,\"position\":\"Director\"},{\"name\":\"Melissa Blau\",\"age\":52,\"position\":\"Director\"},{\"name\":\"Duncan Davidson\",\"age\":68,\"position\":\"Executive Vice President\"},{\"name\":\"Eric Wiesen\",\"age\":46,\"position\":\"President\"},{\"name\":\"David VanEgmond\",\"age\":32,\"position\":\"Chief Executive Officer\"},{\"name\":\"Paul Martino\",\"age\":46,\"position\":\"Executive Chairman\"}],\"underwriters\":[{\"name\":\"Citigroup Global Markets Inc.\"}]}");
  1033.         assert_eq!(aftr_all_json, "{\"tickers\":{\"common\":\"AFTR\",\"units\":\"AFTRU\",\"warrants\":\"AFTRW\"},\"financial\":[{\"type\":\"expense\",\"currency\":\"$\",\"per_unit_value\":0.55,\"total_value\":16500000},{\"type\":\"ipo\",\"currency\":\"$\",\"per_unit_value\":10.0,\"total_value\":300000000}],\"management\":[{\"name\":\"Bharat Sundaram\",\"age\":43,\"position\":\"Director Nominee\"},{\"name\":\"Bill Miller\",\"age\":54,\"position\":\"Director Nominee\"},{\"name\":\"Christopher H. Hunter\",\"age\":52,\"position\":\"Director Nominee\"},{\"name\":\"Dr. Julie Gerberding\",\"age\":65,\"position\":\"Director Nominee\"},{\"name\":\"A.G. Breitenstein\",\"age\":52,\"position\":\"Director Nominee\"},{\"name\":\"Nehal Raj\",\"age\":42,\"position\":\"Director\"},{\"name\":\"Jeffrey Rhodes\",\"age\":46,\"position\":\"Director\"},{\"name\":\"Martin Davidson\",\"age\":45,\"position\":\"Chief Financial Officer\"},{\"name\":\"Anthony Colaluca\",\"age\":54,\"position\":\"President and Director\"},{\"name\":\"R. Halsey Wise\",\"age\":56,\"position\":\"Chief Executive Officer and Chairman\"}],\"underwriters\":[{\"name\":\"BofA Securities, Inc.\"},{\"name\":\"Deutsche Bank Securities Inc.\"},{\"name\":\"Goldman Sachs & Co. LLC.\"}]}");
  1034.     }
  1035. }
  1036.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement