From 6a059c5a4aa0b724e80050f24cbacb934f815f10 Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Sat, 10 Sep 2022 11:41:18 -0500 Subject: [PATCH] Vin result port and passes --- src/client/result/ResultParser.rs | 6 +- src/client/result/VINParsedResult.rs | 188 +++++++------ src/client/result/VINParsedResultTestCase.rs | 140 +++++++--- src/client/result/VINResultParser.rs | 280 +++++++++++-------- 4 files changed, 365 insertions(+), 249 deletions(-) diff --git a/src/client/result/ResultParser.rs b/src/client/result/ResultParser.rs index c90b426..c3f0e2f 100644 --- a/src/client/result/ResultParser.rs +++ b/src/client/result/ResultParser.rs @@ -37,7 +37,7 @@ use super::{ BookmarkDoCoMoResultParser, EmailAddressResultParser, EmailDoCoMoResultParser, GeoResultParser, ISBNResultParser, ParsedClientResult, ParsedRXingResult, ProductResultParser, SMSMMSResultParser, SMTPResultParser, TelResultParser, TextParsedRXingResult, URIResultParser, - URLTOResultParser, WifiResultParser, + URLTOResultParser, VINResultParser, WifiResultParser, }; /** @@ -104,7 +104,7 @@ pub fn getMassagedText(result: &RXingResult) -> String { } pub fn parseRXingResult(theRXingResult: &RXingResult) -> ParsedClientResult { - let PARSERS: [&ParserFunction; 13] = [ + let PARSERS: [&ParserFunction; 14] = [ &BookmarkDoCoMoResultParser::parse, // new AddressBookDoCoMoRXingResultParser(), &EmailDoCoMoResultParser::parse, @@ -124,7 +124,7 @@ pub fn parseRXingResult(theRXingResult: &RXingResult) -> ParsedClientResult { &ISBNResultParser::parse, &ProductResultParser::parse, // new ExpandedProductRXingResultParser(), - // new VINRXingResultParser(), + &VINResultParser::parse, ]; for parser in PARSERS { diff --git a/src/client/result/VINParsedResult.rs b/src/client/result/VINParsedResult.rs index 4a244e2..aee3f70 100644 --- a/src/client/result/VINParsedResult.rs +++ b/src/client/result/VINParsedResult.rs @@ -14,96 +14,110 @@ * limitations under the License. */ - // package com.google.zxing.client.result; +use super::{ParsedRXingResult, ParsedRXingResultType}; + /** * Represents a parsed result that encodes a Vehicle Identification Number (VIN). */ -pub struct VINParsedRXingResult { - - private final String vin; - private final String worldManufacturerID; - private final String vehicleDescriptorSection; - private final String vehicleIdentifierSection; - private final String countryCode; - private final String vehicleAttributes; - private final int modelYear; - private final char plantCode; - private final String sequentialNumber; - - impl ParsedRXingResult for VINParsedRXingResult {} - impl VINParsedRXingResult{ - - public VINParsedRXingResult(String vin, - String worldManufacturerID, - String vehicleDescriptorSection, - String vehicleIdentifierSection, - String countryCode, - String vehicleAttributes, - int modelYear, - char plantCode, - String sequentialNumber) { - super(ParsedRXingResultType.VIN); - this.vin = vin; - this.worldManufacturerID = worldManufacturerID; - this.vehicleDescriptorSection = vehicleDescriptorSection; - this.vehicleIdentifierSection = vehicleIdentifierSection; - this.countryCode = countryCode; - this.vehicleAttributes = vehicleAttributes; - this.modelYear = modelYear; - this.plantCode = plantCode; - this.sequentialNumber = sequentialNumber; - } - - public String getVIN() { - return vin; - } - - public String getWorldManufacturerID() { - return worldManufacturerID; - } - - public String getVehicleDescriptorSection() { - return vehicleDescriptorSection; - } - - public String getVehicleIdentifierSection() { - return vehicleIdentifierSection; - } - - public String getCountryCode() { - return countryCode; - } - - public String getVehicleAttributes() { - return vehicleAttributes; - } - - public int getModelYear() { - return modelYear; - } - - public char getPlantCode() { - return plantCode; - } - - public String getSequentialNumber() { - return sequentialNumber; - } - - @Override - public String getDisplayRXingResult() { - StringBuilder result = new StringBuilder(50); - result.append(worldManufacturerID).append(' '); - result.append(vehicleDescriptorSection).append(' '); - result.append(vehicleIdentifierSection).append('\n'); - if (countryCode != null) { - result.append(countryCode).append(' '); - } - result.append(modelYear).append(' '); - result.append(plantCode).append(' '); - result.append(sequentialNumber).append('\n'); - return result.toString(); - } +pub struct VINParsedRXingResult { + vin: String, + world_manufacturer_id: String, + vehicle_descriptor_section: String, + vehicle_identifier_section: String, + country_code: String, + vehicle_attributes: String, + model_year: u32, + plant_code: char, + sequentialNumber: String, +} + +impl ParsedRXingResult for VINParsedRXingResult { + fn getType(&self) -> super::ParsedRXingResultType { + ParsedRXingResultType::VIN + } + + fn getDisplayRXingResult(&self) -> String { + let mut result = String::with_capacity(50); + result.push_str(&self.world_manufacturer_id); + result.push(' '); + result.push_str(&self.vehicle_descriptor_section); + result.push(' '); + result.push_str(&self.vehicle_identifier_section); + result.push('\n'); + if !self.country_code.is_empty() { + result.push_str(&self.country_code); + result.push(' '); + } + result.push_str(&self.model_year.to_string()); + result.push(' '); + result.push(self.plant_code); + result.push(' '); + result.push_str(&self.sequentialNumber); + result.push('\n'); + + result + } +} +impl VINParsedRXingResult { + pub fn new( + vin: String, + worldManufacturerID: String, + vehicleDescriptorSection: String, + vehicleIdentifierSection: String, + countryCode: String, + vehicleAttributes: String, + modelYear: u32, + plantCode: char, + sequentialNumber: String, + ) -> Self { + Self { + vin, + world_manufacturer_id: worldManufacturerID, + vehicle_descriptor_section: vehicleDescriptorSection, + vehicle_identifier_section: vehicleIdentifierSection, + country_code: countryCode, + vehicle_attributes: vehicleAttributes, + model_year: modelYear, + plant_code: plantCode, + sequentialNumber, + } + } + + pub fn getVIN(&self) -> &str { + &self.vin + } + + pub fn getWorldManufacturerID(&self) -> &str { + &self.world_manufacturer_id + } + + pub fn getVehicleDescriptorSection(&self) -> &str { + &self.vehicle_descriptor_section + } + + pub fn getVehicleIdentifierSection(&self) -> &str { + &self.vehicle_identifier_section + } + + pub fn getCountryCode(&self) -> &str { + &self.country_code + } + + pub fn getVehicleAttributes(&self) -> &str { + &self.vehicle_attributes + } + + pub fn getModelYear(&self) -> u32 { + self.model_year + } + + pub fn getPlantCode(&self) -> char { + self.plant_code + } + + pub fn getSequentialNumber(&self) -> &str { + &self.sequentialNumber + } } diff --git a/src/client/result/VINParsedResultTestCase.rs b/src/client/result/VINParsedResultTestCase.rs index 5dd2c3f..8332b82 100644 --- a/src/client/result/VINParsedResultTestCase.rs +++ b/src/client/result/VINParsedResultTestCase.rs @@ -14,57 +14,107 @@ * limitations under the License. */ +// package com.google.zxing.client.result; -package com.google.zxing.client.result; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.RXingResult; -import org.junit.Assert; -import org.junit.Test; +// import com.google.zxing.BarcodeFormat; +// import com.google.zxing.RXingResult; +// import org.junit.Assert; +// import org.junit.Test; /** * Tests {@link VINParsedRXingResult}. */ -public final class VINParsedRXingResultTestCase extends Assert { +// public final class VINParsedRXingResultTestCase extends Assert { +use crate::{ + client::result::{ParsedClientResult, ParsedRXingResult, ParsedRXingResultType}, + BarcodeFormat, RXingResult, +}; - @Test - public void testNotVIN() { - RXingResult fakeRXingResult = new RXingResult("1M8GDM9A1KP042788", null, null, BarcodeFormat.CODE_39); - ParsedRXingResult result = RXingResultParser.parseRXingResult(fakeRXingResult); - assertEquals(ParsedRXingResultType.TEXT, result.getType()); - fakeRXingResult = new RXingResult("1M8GDM9AXKP042788", null, null, BarcodeFormat.CODE_128); - result = RXingResultParser.parseRXingResult(fakeRXingResult); - assertEquals(ParsedRXingResultType.TEXT, result.getType()); - } +use super::ResultParser; - @Test - public void testVIN() { - doTest("1M8GDM9AXKP042788", "1M8", "GDM9AX", "KP042788", "US", "GDM9A", 1989, 'P', "042788"); - doTest("I1M8GDM9AXKP042788", "1M8", "GDM9AX", "KP042788", "US", "GDM9A", 1989, 'P', "042788"); - doTest("LJCPCBLCX11000237", "LJC", "PCBLCX", "11000237", "CN", "PCBLC", 2001, '1', "000237"); - } +#[test] +fn testNotVIN() { + let fakeRXingResult = RXingResult::new( + "1M8GDM9A1KP042788", + Vec::new(), + Vec::new(), + BarcodeFormat::CODE_39, + ); + let result = ResultParser::parseRXingResult(&fakeRXingResult); + assert_eq!(ParsedRXingResultType::TEXT, result.getType()); + let fakeRXingResult = RXingResult::new( + "1M8GDM9AXKP042788", + Vec::new(), + Vec::new(), + BarcodeFormat::CODE_128, + ); + let result = ResultParser::parseRXingResult(&fakeRXingResult); + assert_eq!(ParsedRXingResultType::TEXT, result.getType()); +} - private static void doTest(String contents, - String wmi, - String vds, - String vis, - String country, - String attributes, - int year, - char plant, - String sequential) { - RXingResult fakeRXingResult = new RXingResult(contents, null, null, BarcodeFormat.CODE_39); - ParsedRXingResult result = RXingResultParser.parseRXingResult(fakeRXingResult); - assertSame(ParsedRXingResultType.VIN, result.getType()); - VINParsedRXingResult vinRXingResult = (VINParsedRXingResult) result; - assertEquals(wmi, vinRXingResult.getWorldManufacturerID()); - assertEquals(vds, vinRXingResult.getVehicleDescriptorSection()); - assertEquals(vis, vinRXingResult.getVehicleIdentifierSection()); - assertEquals(country, vinRXingResult.getCountryCode()); - assertEquals(attributes, vinRXingResult.getVehicleAttributes()); - assertEquals(year, vinRXingResult.getModelYear()); - assertEquals(plant, vinRXingResult.getPlantCode()); - assertEquals(sequential, vinRXingResult.getSequentialNumber()); - } +#[test] +fn testVIN() { + doTest( + "1M8GDM9AXKP042788", + "1M8", + "GDM9AX", + "KP042788", + "US", + "GDM9A", + 1989, + 'P', + "042788", + ); + doTest( + "I1M8GDM9AXKP042788", + "1M8", + "GDM9AX", + "KP042788", + "US", + "GDM9A", + 1989, + 'P', + "042788", + ); + doTest( + "LJCPCBLCX11000237", + "LJC", + "PCBLCX", + "11000237", + "CN", + "PCBLC", + 2001, + '1', + "000237", + ); +} -} \ No newline at end of file +fn doTest( + contents: &str, + wmi: &str, + vds: &str, + vis: &str, + country: &str, + attributes: &str, + year: u32, + plant: char, + sequential: &str, +) { + let fakeRXingResult = + RXingResult::new(contents, Vec::new(), Vec::new(), BarcodeFormat::CODE_39); + let result = ResultParser::parseRXingResult(&fakeRXingResult); + assert_eq!(ParsedRXingResultType::VIN, result.getType()); + if let ParsedClientResult::VINResult(vinRXingResult) = result { + // let vinRXingResult = (VINParsedRXingResult) result; + assert_eq!(wmi, vinRXingResult.getWorldManufacturerID()); + assert_eq!(vds, vinRXingResult.getVehicleDescriptorSection()); + assert_eq!(vis, vinRXingResult.getVehicleIdentifierSection()); + assert_eq!(country, vinRXingResult.getCountryCode()); + assert_eq!(attributes, vinRXingResult.getVehicleAttributes()); + assert_eq!(year, vinRXingResult.getModelYear()); + assert_eq!(plant, vinRXingResult.getPlantCode()); + assert_eq!(sequential, vinRXingResult.getSequentialNumber()); + } else { + panic!("Expected VINResult"); + } +} diff --git a/src/client/result/VINResultParser.rs b/src/client/result/VINResultParser.rs index 4640fd4..369a321 100644 --- a/src/client/result/VINResultParser.rs +++ b/src/client/result/VINResultParser.rs @@ -23,7 +23,9 @@ use regex::Regex; -use crate::{RXingResult, BarcodeFormat, exceptions::Exceptions}; +use crate::{ + client::result::VINParsedRXingResult, exceptions::Exceptions, BarcodeFormat, RXingResult, +}; use super::ParsedClientResult; @@ -33,81 +35,110 @@ use super::ParsedClientResult; * @author Sean Owen */ pub fn parse(result: &RXingResult) -> Option { - if result.getBarcodeFormat() != &BarcodeFormat::CODE_39 { - return None; - } - let ioq_matcher = Regex::new(IOQ).unwrap(); - let az09_matcher = Regex::new(AZ09).unwrap(); - let rawText = result.getText(); - ioq_matcher.replace_all(result.getText(), "").trim(); - // rawText = IOQ.matcher(rawText).replaceAll("").trim(); - if let None = az09_matcher.find(rawText){ - return None; - } - // if !AZ09.matcher(rawText).matches() { - // return null; - // } - let check_cs = checkChecksum(rawText); - if !check_cs { - return None; - } - let wmi = &rawText[..3]; - try { + if result.getBarcodeFormat() != &BarcodeFormat::CODE_39 { + return None; + } + let ioq_matcher = Regex::new(IOQ).unwrap(); + let az09_matcher = Regex::new(AZ09).unwrap(); + let rawText_res = result.getText().trim(); + let rawText = ioq_matcher.replace_all(rawText_res, "").to_string(); + // rawText = IOQ.matcher(rawText).replaceAll("").trim(); + if let None = az09_matcher.find(&rawText) { + return None; + } + // if !AZ09.matcher(rawText).matches() { + // return null; + // } + let check_cs = checkChecksum(&rawText).unwrap_or(false); + if !check_cs { + return None; + } + let wmi = &rawText[..3]; + // try { // if (!checkChecksum(rawText)) { // return null; // } // String wmi = rawText.substring(0, 3); - return new VINParsedRXingResult(rawText, - wmi, - rawText.substring(3, 9), - rawText.substring(9, 17), - countryCode(wmi), - rawText.substring(3, 8), - modelYear(rawText.charAt(9)), - rawText.charAt(10), - rawText.substring(11)); - } catch (IllegalArgumentException iae) { - return null; - } + let country_code = countryCode(wmi).unwrap_or(""); + let model_year = modelYear(rawText.chars().nth(9).unwrap_or('_')); + if model_year.is_err() { + return None; + } + Some(ParsedClientResult::VINResult(VINParsedRXingResult::new( + rawText.to_owned(), + wmi.to_owned(), + rawText[3..9].to_owned(), + rawText[9..17].to_owned(), + country_code.to_owned(), + rawText[3..8].to_owned(), + model_year.unwrap(), + rawText.chars().nth(10).unwrap(), + rawText[11..].to_owned(), + ))) + // return new VINParsedRXingResult(rawText, + // wmi, + // rawText.substring(3, 9), + // rawText.substring(9, 17), + // countryCode(wmi), + // rawText.substring(3, 8), + // modelYear(rawText.charAt(9)), + // rawText.charAt(10), + // rawText.substring(11)); + // } catch (IllegalArgumentException iae) { + // return null; + // } } - const IOQ : &'static str = "[IOQ]"; - const AZ09 :&'static str = "[A-Z0-9]{17}"; +const IOQ: &'static str = "[IOQ]"; +const AZ09: &'static str = "[A-Z0-9]{17}"; - fn checkChecksum( vin: &str) -> Result { +fn checkChecksum(vin: &str) -> Result { let mut sum = 0; for i in 0..vin.len() { - // for (int i = 0; i < vin.length(); i++) { - sum += vinPositionWeight(i + 1)? * vinCharValue(vin.chars().nth(i).unwrap())?; + // for (int i = 0; i < vin.length(); i++) { + sum += vinPositionWeight(i + 1)? as u32 * vinCharValue(vin.chars().nth(i).unwrap())?; } let checkToChar = vin.chars().nth(8).unwrap(); let expectedCheckChar = checkChar((sum % 11) as u8)?; - Ok( checkToChar == expectedCheckChar) - } - - fn vinCharValue( c:char)->Result { - if c >= 'A' && c <= 'I' { - return Ok((c as u8 as u32 - 'A' as u8 as u32) + 1); + Ok(checkToChar == expectedCheckChar) +} + +fn vinCharValue(c: char) -> Result { + match c { + 'A'..='I' => Ok((c as u8 as u32 - 'A' as u8 as u32) + 1), + 'J'..='R' => Ok((c as u8 as u32 - 'J' as u8 as u32) + 1), + 'S'..='Z' => Ok((c as u8 as u32 - 'S' as u8 as u32) + 2), + '0'..='9' => Ok(c as u8 as u32 - '0' as u8 as u32), + _ => Err(Exceptions::IllegalArgumentException( + "vin char out of range".to_owned(), + )), } - if c >= 'J' && c <= 'R' { - return Ok((c as u8 as u32 - 'J' as u8 as u32) + 1); - } - if c >= 'S' && c <= 'Z' { - return Ok((c as u8 as u32- 'S' as u8 as u32) + 2); - } - if c >= '0' && c <= '9' { - return Ok(c as u8 as u32 - '0' as u8 as u32); - } - Err( Exceptions::IllegalArgumentException("vin char out of range".to_owned())) - } - - fn vinPositionWeight( position:usize) -> Result{ + // if c >= 'A' && c <= 'I' { + // return Ok((c as u8 as u32 - 'A' as u8 as u32) + 1); + // } + // if c >= 'J' && c <= 'R' { + // return Ok((c as u8 as u32 - 'J' as u8 as u32) + 1); + // } + // if c >= 'S' && c <= 'Z' { + // return Ok((c as u8 as u32 - 'S' as u8 as u32) + 2); + // } + // if c >= '0' && c <= '9' { + // return Ok(c as u8 as u32 - '0' as u8 as u32); + // } + // Err(Exceptions::IllegalArgumentException( + // "vin char out of range".to_owned(), + // )) +} + +fn vinPositionWeight(position: usize) -> Result { match position { - 1..=7 => Ok(9-position), - 8 => Ok(10), - 9 => Ok(0), - 10..=17 => Ok(19-position), - _ => Err(Exceptions::IllegalArgumentException("vin position weight out of bounds".to_owned())), + 1..=7 => Ok(9 - position), + 8 => Ok(10), + 9 => Ok(0), + 10..=17 => Ok(19 - position), + _ => Err(Exceptions::IllegalArgumentException( + "vin position weight out of bounds".to_owned(), + )), } // if position >= 1 && position <= 7 { // return 9 - position; @@ -122,62 +153,83 @@ pub fn parse(result: &RXingResult) -> Option { // return 19 - position; // } // throw new IllegalArgumentException(); - } +} - fn checkChar( remainder:u8) -> Result { - if remainder < 10 { - return Ok( ('0' as u8 + remainder) as char ); +fn checkChar(remainder: u8) -> Result { + match remainder { + 0..=9 => Ok(('0' as u8 + remainder) as char), + 10 => Ok('X'), + _ => Err(Exceptions::IllegalArgumentException( + "remainder too high".to_owned(), + )), } - if remainder == 10 { - return Ok('X'); - } - Err(Exceptions::IllegalArgumentException("remainder too high".to_owned())) - } - - fn modelYear( c:char) -> Result { - if (c >= 'E' && c <= 'H') { - return (c - 'E') + 1984; - } - if (c >= 'J' && c <= 'N') { - return (c - 'J') + 1988; - } - if (c == 'P') { - return 1993; - } - if (c >= 'R' && c <= 'T') { - return (c - 'R') + 1994; - } - if (c >= 'V' && c <= 'Y') { - return (c - 'V') + 1997; - } - if (c >= '1' && c <= '9') { - return (c - '1') + 2001; - } - if (c >= 'A' && c <= 'D') { - return (c - 'A') + 2010; - } - throw new IllegalArgumentException(); - } + // if remainder < 10 { + // return Ok(('0' as u8 + remainder) as char); + // } + // if remainder == 10 { + // return Ok('X'); + // } + // Err(Exceptions::IllegalArgumentException( + // "remainder too high".to_owned(), + // )) +} - fn countryCode( wmi : &str) -> Option<&'static str> { +fn modelYear(c: char) -> Result { + match c { + 'E'..='H' => Ok((c as u8 as u32 - 'E' as u8 as u32) + 1984), + 'J'..='N' => Ok((c as u8 as u32 - 'J' as u8 as u32) + 1988), + 'P' => Ok(1993), + 'R'..='T' => Ok((c as u8 as u32 - 'R' as u8 as u32) + 1994), + 'V'..='Y' => Ok((c as u8 as u32 - 'V' as u8 as u32) + 1997), + '1'..='9' => Ok((c as u8 as u32 - '1' as u8 as u32) + 2001), + 'A'..='D' => Ok((c as u8 as u32 - 'A' as u8 as u32) + 2010), + _ => Err(Exceptions::IllegalArgumentException(String::from( + "model year argument out of range", + ))), + } + // if c >= 'E' && c <= 'H' { + // return (c - 'E') + 1984; + // } + // if c >= 'J' && c <= 'N' { + // return (c - 'J') + 1988; + // } + // if c == 'P' { + // return 1993; + // } + // if c >= 'R' && c <= 'T' { + // return (c - 'R') + 1994; + // } + // if c >= 'V' && c <= 'Y' { + // return (c - 'V') + 1997; + // } + // if c >= '1' && c <= '9' { + // return (c - '1') + 2001; + // } + // if c >= 'A' && c <= 'D' { + // return (c - 'A') + 2010; + // } + // throw new IllegalArgumentException(); +} + +fn countryCode(wmi: &str) -> Option<&'static str> { let c1 = wmi.chars().nth(0).unwrap(); let c2 = wmi.chars().nth(1).unwrap(); match c1 { - '1' | '4'| '5' => Some("US"), - '2' => Some("CA"), - '3' if c2 >= 'A' && c2 <= 'W' => Some("MX"), - '9' if ((c2 >= 'A' && c2 <= 'E') || (c2 >= '3' && c2 <= '9')) => Some("BR"), -'J' if (c2 >= 'A' && c2 <= 'T') => Some("JP"), -'K' if (c2 >= 'L' && c2 <= 'R') => Some("KO"), -'L' => Some("CN"), -'M' if (c2 >= 'A' && c2 <= 'E') => Some("IN"), -'S' if (c2 >= 'A' && c2 <= 'M') => Some("UK"), -'S' if (c2 >= 'N' && c2 <= 'T') => Some("DE"), -'V' if (c2 >= 'F' && c2 <= 'R') => Some("FR"), -'V' if (c2 >= 'S' && c2 <= 'W') => Some("ES"), -'W' => Some("DE"), -'X' if (c2 == '0' || (c2 >= '3' && c2 <= '9')) => Some("RU"), -'Z' if (c2 >= 'A' && c2 <= 'R') => Some("IT"), -_ => None + '1' | '4' | '5' => Some("US"), + '2' => Some("CA"), + '3' if c2 >= 'A' && c2 <= 'W' => Some("MX"), + '9' if ((c2 >= 'A' && c2 <= 'E') || (c2 >= '3' && c2 <= '9')) => Some("BR"), + 'J' if (c2 >= 'A' && c2 <= 'T') => Some("JP"), + 'K' if (c2 >= 'L' && c2 <= 'R') => Some("KO"), + 'L' => Some("CN"), + 'M' if (c2 >= 'A' && c2 <= 'E') => Some("IN"), + 'S' if (c2 >= 'A' && c2 <= 'M') => Some("UK"), + 'S' if (c2 >= 'N' && c2 <= 'T') => Some("DE"), + 'V' if (c2 >= 'F' && c2 <= 'R') => Some("FR"), + 'V' if (c2 >= 'S' && c2 <= 'W') => Some("ES"), + 'W' => Some("DE"), + 'X' if (c2 == '0' || (c2 >= '3' && c2 <= '9')) => Some("RU"), + 'Z' if (c2 >= 'A' && c2 <= 'R') => Some("IT"), + _ => None, } - } \ No newline at end of file +}