Correlating a named file with Standard Input
The complete source of standardTypes_data_correlation.ek9, compiled by the EK9 compiler. Shown on standardTypes.html.
#!ek9 defines module site.standardTypes.dataCorrelation defines program DataCorrelation() -> argv as List of String stdin <- Stdin() stdout <- Stdout() stderr <- Stderr() optionsHandler <- OptionsHandler() options <- optionsHandler.processCommandLine(argv) verboseMode <- options contains "-v" debugLevel <- optionsHandler.processDebugLevel(verboseMode, options.getOrDefault("-d", "")) //Use NamedFileProcessor for clean separation of concerns filename <- Optional(options.getOrDefault("-f", "")) namedFileContents <- NamedFileProcessor(verboseMode, debugLevel).processNamedFile(filename) if verboseMode stderr.println(`Process id is [${OS().pid()}]`) if verboseMode or debugLevel > 0 stderr.println(`Loaded ${$ length namedFileContents} records from named file`) setupSignalHandling(verboseMode, debugLevel) validLine <- createLineValidator(stderr, debugLevel) toEntry <- createStdinLineHandler(stderr, debugLevel) byMerging <- createCustomerRecordMerger(stderr, debugLevel, namedFileContents) if debugLevel > 1 stderr.println("Ready to start processing Standard Input") outputHeader(verboseMode, debugLevel, stdout) cat stdin | filter by validLine | map toEntry | filter by validEntry | map byMerging > stdout if debugLevel > 1 stderr.println("Standard Input processing complete") defines function lineValidator() as pure abstract -> line as String <- rtn as Boolean? lineToCustomerRecord() as pure open -> line as String <- rtn as DictEntry of (String, CustomerRecord)? require line? rtn: DictEntry() of (String, CustomerRecord) customerRecordMerger() as abstract -> entry as DictEntry of (String, CustomerRecord) <- rtn as String? createLineValidator() -> stderr as Stderr debugLevel as Integer <- validator as lineValidator? validator: (stderr, debugLevel) is lineValidator as pure function rtn :=? line is not empty and #<line != '#' if debugLevel > 1 and not rtn stderr.println(`Discarding [${line}]`) createStdinLineHandler() -> stderr as Stderr debugLevel as Integer <- processor as lineToCustomerRecord? processor: (stderr, debugLevel) is lineToCustomerRecord as pure function rtn: DictEntry() of (String, CustomerRecord) traceLevel <- 3 expectedFieldCount <- 4 splitLine <- line.split(/,/) if debugLevel == traceLevel stderr.println(`About to split stdin line [${line}]`) if length of splitLine == expectedFieldCount id <- splitLine.getOrDefault(0, "").trim() firstname <- splitLine.getOrDefault(1, "").trim() lastname <- splitLine.getOrDefault(2, "").trim() dobStr <- splitLine.getOrDefault(3, "").trim() if id is not empty and firstname is not empty and lastname is not empty and dobStr is not empty dob <- $getAsDateTime(id, false, dobStr) if dob? entryValue <- CustomerRecord(id: id, firstName: firstname, lastName: lastname, email: String(), dob: dob, lastPurchase: String()) if debugLevel == traceLevel stderr.println(`Line [${line}] processed`) rtn :=? DictEntry(id, entryValue) if not rtn? stderr.println(`Invalid line [${line}]`) getAsDateTime() as pure -> id as String throwException as Boolean aDateInput as String //expecting YYYYMMDD <- rtn as DateTime: DateTime() expectedDateParts <- 3 group <- aDateInput.group(/(\d{4})(\d{2})(\d{2})/) if length group == expectedDateParts dateStr <- cat group | join with dashSeparated | collect as String rtn := DateTime(Date(dateStr)) else if throwException throw Exception(`Id [${id}] Invalid date [${aDateInput}]`, 2) else Stderr().println(`Id [${id}] Invalid date [${aDateInput}]`) validEntry() as pure -> entry as DictEntry of (String, CustomerRecord) <- rtn as Boolean: entry? dashSeparated() as pure -> firstPart String secondPart String <- rtn as String: firstPart? and secondPart? <- `${firstPart}-${secondPart}` : String() outputFormatSeparated() as pure -> firstPart String secondPart String <- rtn as String: firstPart? and secondPart? <- `${firstPart}, ${secondPart}` : String() setupSignalHandling() -> verboseMode as Boolean debugLevel as Integer terminationHandler <- (verboseMode, debugLevel) is SignalHandler as function result: 1 //The error code to terminate with if verboseMode or debugLevel > 0 Stderr().println(`Handled Terminal Signal [${signal}]`) terminations <- Signals().register(["HUP", "ABRT"], terminationHandler) if terminations not contains "HUP" Stderr().println("HUP Signal not supported") if terminations not contains "ABRT" Stderr().println("ABRT Signal not supported") debugHandler <- (verboseMode, debugLevel) is SignalHandler as function maxDebugLevel <- 3 result: Integer() //Note the Integer is not set so process will not terminate if signal == "USR1" and debugLevel < maxDebugLevel debugLevel++ else if signal == "USR2" and debugLevel > 0 debugLevel-- if verboseMode or debugLevel > 0 Stderr().println(`Handled Info Signal [${signal}] Debug Level now [${debugLevel}]`) debugs <- Signals().register(["USR1", "USR2"], debugHandler) if debugs not contains "USR1" Stderr().println("USR1 Signal not supported") if debugs not contains "USR2" Stderr().println("USR2 Signal not supported") outputHeader() -> verboseMode as Boolean debugLevel as Integer stdout as Stdout detailedDebugLevel <- 2 enGB <- Locale("en_GB") if verboseMode or debugLevel > detailedDebugLevel Stderr().println(`About to create output header with locale ${enGB}`) envVars <- EnvVars() user <- envVars contains "USER" <- envVars.get("USER") else envVars.get("USERNAME") dateTime <- enGB.mediumFormat(SystemClock().dateTime()) stdout.println(`#Created by ${user} on ${dateTime}`) stdout.println("#id, firstName, lastName, emailAddress, dateOfBirth, dateLastPurchase") createCustomerRecordMerger() -> stderr as Stderr debugLevel as Integer namedFileContents as Dict of (String, CustomerRecord) <- merge as customerRecordMerger? merge: (stderr, debugLevel, namedFileContents) is customerRecordMerger traceLevel <- 3 rtn: String() //Output will be unset by default and there for not output id <- entry.key() if debugLevel == traceLevel stderr.println(`Merging id [${id}]`) if namedFileContents contains id stdinEntry <- entry.value() namedFileEntry <- namedFileContents.getOrDefault(id, CustomerRecord()) stdinEntry :~: namedFileEntry rtn := $stdinEntry else stderr.println(`Not merging [${id}] as named file does not contain id`) defines class OptionsHandler processCommandLine() as pure -> arguments as List of String <- options as Dict of (String, String)? getopts <- setupGetOpt(":") options :=? getopts.options(arguments) private setupGetOpt() as pure -> rqParam as String <- getopts as GetOpt? supportedOptions <- setupSupportedOptions(rqParam) usage <- setupUsage() getopts :=? GetOpt(rqParam, supportedOptions, usage) private setupSupportedOptions() as pure -> rqParam as String <- rtn as Dict of (String, String)? rtn :=? { "-v": String(), "-f": rqParam, "-d": rqParam } private setupUsage() as pure <- rtn as String? desc <- "Invalid option, only those list below are supported:" verbose <- "-v, verbose" filename <- "-f filename, use file of filename (mandatory option)" debug <- "-d level, use of debugging" rtn :=? `${desc}\n${verbose}\n${filename}\n${debug}` processDebugLevel() -> verboseMode as Boolean level as String <- rtn as Integer: 0 //default to zero maxDebugLevel <- 3 debugLevel <- Integer(level) rtn := (debugLevel in 0 ... maxDebugLevel) <- debugLevel else 0 if verboseMode and (debugLevel not in 0 ... maxDebugLevel) Stderr().println(`Debug level ${debugLevel} not supported - reverting to debug level '0'`) NamedFileProcessor verboseMode as Boolean: false debugLevel as Integer: Integer() NamedFileProcessor() -> verboseMode as Boolean debugLevel as Integer this.verboseMode = verboseMode this.debugLevel = debugLevel processNamedFile() -> filename as Optional of String <- rtn <- Dict() of (String, CustomerRecord) stderr <- Stderr() if filename? namedFile <- getNamedFile(filename.get()) validLine <- createLineValidator(stderr, debugLevel) toEntry <- createNamedFileLineHandler(stderr, debugLevel) if verboseMode or debugLevel > 0 stderr.println(`About to start processing [${namedFile}]`) cat namedFile.input() | filter by validLine | map toEntry > rtn if debugLevel > 1 stderr.println(`Processing [${namedFile}] complete`) else throw Exception("Filename of 'named file' is required", 2) private getNamedFile() -> filename as String <- rtn as TextFile: TextFile() filePath <- FileSystemPath(filename) rtn := filePath.isAbsolute() <- TextFile(filePath) else TextFile(FileSystem().cwd() + filePath) if not rtn? throw Exception(`${filePath} is not an existing file`, 2) if not rtn.isReadable() throw Exception(`${rtn} is not readable`, 2) private createNamedFileLineHandler() as pure -> stderr as Stderr debugLevel as Integer <- processor as lineToCustomerRecord? processor :=? (stderr, debugLevel) is lineToCustomerRecord as pure function traceLevel <- 3 expectedFieldCount <- 4 splitLine <- line.split(/:/) if debugLevel == traceLevel stderr.println(`About to split line [${line}]`) if length of splitLine != expectedFieldCount throw Exception(`Invalid line [${line}]`, 2) id <- splitLine.getOrDefault(0, "").trim() if id is empty throw Exception(`Invalid line [${line}] empty ID`, 2) emailAddress <- splitLine.getOrDefault(1, "").trim() if emailAddress not matches /[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/ throw Exception(`Id [${id}] Invalid email address [${emailAddress}]`, 2) dateLastPurchase <- $getAsDateTime(id, true, splitLine.getOrDefault(3, "").trim()) entryValue <- CustomerRecord(id: id, firstName: String(), lastName: String(), email: emailAddress, dob: String(), lastPurchase: dateLastPurchase) if debugLevel == traceLevel stderr.println(`Named file line [${line}] processed`) rtn :=? DictEntry(id, entryValue) default operator ? defines record CustomerRecord id <- String() firstName <- String() lastName <- String() email <- String() dob <- String() lastPurchase <- String() default CustomerRecord() as pure CustomerRecord() as pure -> id as String firstName as String lastName as String email as String dob as String lastPurchase as String this.id :=? String(id) this.firstName :=? String(firstName) this.lastName :=? String(lastName) this.email :=? String(email) this.dob :=? String(dob) this.lastPurchase :=? String(lastPurchase) override operator ? as pure <- rtn as Boolean: id? operator :~: -> other as CustomerRecord id :=? String(other.id) firstName :=? String(other.firstName) lastName :=? String(other.lastName) email :=? String(other.email) dob :=? String(other.dob) lastPurchase :=? String(other.lastPurchase) operator $ as pure <- rtn as String: cat [id, firstName, lastName, email, dob, lastPurchase] | join with outputFormatSeparated | collect as String