#!/usr/bin/perl -w # # This Perl script converts text copies of Robinhood's YR2020 # 1099s into CSV format to save time for accountants. # Supports stock and options trades. It's FREEWARE. # # Robinhood produces annual tax documents (form 1099s) which can # be downloaded in PDF format. These files contain records of # stock and options sales. The PDF files must first be converted # to text format. In Windows, this can be easily accomplished # with Foxit PDF Reader. Simply, open the PDF file. Click on the # File menu, then click on "Save As." Select type "TXT files," # and click on the Save button. After the PDF file is saved as # text, copy this program into the same directory and run it. # # Written by Zsolt Nagy-Perge # in March 2021, Pensacola, Fla. # ################################################## # # This program was made with Notepad2 and was tested # with TinyPerl 5.8 running in Windows XP. # ################################################## use strict; use warnings; ################################################## ## ## GLOBAL VARIABLES ## my $MONTHS = 'JanFebMarAprMayJunJulAugSepOctNovDec'; # Here's a list of popular securities (CUSIP=SYMBOL) # downloaded from https://stockzoa.com/ticker/qqq # This list must be separated by spaces, # and it must start and end with a space: # Each CUSIP number must start with the letters 'CUS' my $SYMBOL_LIST = uc(' CUS46090E103=QQQ CUS78462F103=SPY CUS88160R101=TSLA CUS037833100=AAPL CUS594918104=MSFT CUS217204106=CPRT CUS060505104=BAC CUS172967424=C CUS020002101=ALL CUS89417E109=TRV CUS269246401=ETFC CUS808513105=SCHW CUS949746101=WFC CUS617446448=MS CUS38141G104=GS CUS084670702=BRKB CUS315616102=FFIV CUS369604103=GE CUS64110L106=NFLX CUS023135106=AMZN CUS097023105=BA CUS74347B268=SRTY CUS74347G861=SQQQ CUS74347W114=ZSL CUS74347W148=UVXY CUS74347X864=UPRO CUS46428Q109=SLV CUS78463V107=GLD CUS25459W540=TMF CUS464287432=TLT CUS511795106=LAKE CUS30231G102=XOM CUS254687106=DIS CUS244199105=DE CUS539830109=LMT CUS717081103=PFE CUS654106103=NKE CUS45773H201=INO CUS25460E182=SOXS CUS25460G690=SOXS CUS22542D316=UGLD CUS129500104=CAL CUS22542D332=TVIXF CUS708160106=JCPNQ CUS85207U105=S CUS92343V104=VZ CUS90130A200=FOX CUS98986T108=ZNGA CUS36467W109=GME CUS437076102=HD CUS548661107=LOW CUS931142103=WMT CUS438516106=HON CUS17275R102=CSCO CUS191216100=KO CUS375558103=GILD CUS31428X106=FDX CUS461202103=INTU CUS171340102=CHD CUS67066G104=NVDA CUS458140100=INTC CUS007903107=AMD CUS219350105=AMAT CUS219350105=GLW CUS02209S103=MO CUS88579Y101=MMM CUS247361702=DAL CUS477143101=JBLU '); my $CUSIP_DB = ''; my @WORDS; my @GRAND_TOTAL; # Grand Total: SOLD, COST, WASH, PROFIT my $OUTPUT = ''; # output buffer my $PATTERN = ''; # Line content pattern to match my $NAME = ''; # Company Name my $TYPE = ''; # PUT or CALL my $SYMBOL = ''; # Stock Symbol my $CUSIP = ''; # Stock ID No. my $QTY = ''; # Quantity my $DATE_OPEN = ''; # Date when position was opened (Acquired) my $DATE_CLOSE = ''; # Date when position was closed (Sold) my $EXP_DATE = ''; # Option Expiration Date my $STRIKE = ''; # Option Strike Price my $SOLD = ''; # Sale/Proceeds my $COST = ''; # Cost/Basis my $WASH_SALE = ''; # Wash sale my $PROFIT = ''; # Gain or loss my $DESCRIPTION = ''; # Description of stock or option my $RAW_DATA = ''; # contains a single line of data read from the file my $BLANK = ''; # a space holder for columns that are intentionally left blank my $NO = 0; # line numbers ################################################## ## ## PROGRAM BEGINS HERE ## About(); # Print the description of this program PrintDottedLine(); # CREATE OUTPUT FILE HEADER: # The header is going to be the first row in the CSV file. # Each name here corresponds to a variable name in the program, # so if you change the name in the header, you must also modify # the name of that variable everywhere in the program! my $HEADER = "DESCRIPTION, DATE_OPEN, DATE_CLOSE, SOLD, COST, WASH_SALE, PROFIT, BLANK, NO, CUSIP, SYMBOL, NAME, QTY, TYPE, EXP_DATE, STRIKE, PATTERN, RAW_DATA\r\n"; my $TEMPLATE = $HEADER; $TEMPLATE =~ s/NO/0/; # The transaction number heading will be changed to 0 $TEMPLATE =~ s/BLANK//; # Remove blank column headings $TEMPLATE =~ tr/,/|/; # We change all the commas to '|' temporarily $TEMPLATE =~ tr|_| |; # Next, we change all the '_' to spaces $TEMPLATE = TitleCase($TEMPLATE); # Format the first row $HEADER =~ s/[ ,\t]+/|\$/g; # Insert $ signs in front of header labels $HEADER = '$' . $HEADER; # Find all text files in the current directory. my $MYPATH = GetCurrentWorkingDirectory(); my $CUSIP_FILE = FormatPath($MYPATH, 'ALLCUSIP.CSV'); my @FILES = ReadDIR($MYPATH, 'txt text'); # Look for Robinhood text files. my $FOUND = 0; print "Searching for Robinhood text files...\n\nDirectory of $MYPATH\n"; foreach my $INPUT_FILE (@FILES) { my $FILESIZE = -s $INPUT_FILE; print "\nChecking: ", $INPUT_FILE; $FILESIZE or next; my $PREVIEW = ReadFile($INPUT_FILE, 0, 1000); # Read first 1000 bytes $PREVIEW =~ tr| a-zA-Z||cd; # Convert UTF-8 to plain text if (index($PREVIEW, 'Robinhood Securities LLC') >= 0) { ParseRobinhoodFile($INPUT_FILE); $FOUND++; } } PrintDottedLine(); print($FOUND ? "PROCESSED: $FOUND ROBINHOOD FILE(s)" : "NONE FOUND!"); PAUSE(); exit; # END OF PROGRAM ################################################## # This function reads an entire Robinhood 1099 # text file and converts it to CSV format. # Usage: ParseRobinhoodFile(FILE_NAME) # sub ParseRobinhoodFile { # Create output file name by changing the extension to .csv my $FILENAME = defined $_[0] ? $_[0] : ''; my $P = rindex($FILENAME, '.'); return if ($P < 0); my $OUTPUT_FILE = substr($FILENAME, 0, $P) . '.csv'; # Create the header for the output file $NO = 0; $OUTPUT = $TEMPLATE; $NAME = $SYMBOL = $CUSIP = $TYPE = $QTY = $DATE_OPEN = $DATE_CLOSE = $COST = $SOLD = $WASH_SALE = $PROFIT = $PATTERN = ''; # Read the entire text file. my $DATA = ReadFile($FILENAME); $DATA =~ tr|\x00-\x09\x0B-\x1F\x7F-\xFF||d; # Convert UTF-8 to ASCII $DATA =~ s/[ \t]+/ /g; # Collapse whitespace my @LINES = split(/\n/, $DATA); # Split lines # Read line by line. @GRAND_TOTAL = (0, 0, 0, 0); for (my $i = 0; $i < @LINES; $i++) { $RAW_DATA = Trim($LINES[$i]); # Trim line @WORDS = split(/[\x00-\x20]+/, $RAW_DATA); # Split into words # In this program, we are looking for complex patterns in a # text file, but instead of using mile-long regex patterns # which can be a nightmare to read, we work backwards: # We build a simple letter-based pattern from each line, # and then we extract values when we find a familiar pattern. $PATTERN = CreatePattern(@WORDS); # Categorize each word my $W = ''; if (Find($PATTERN, 'FWCFW')) # STOCK TRANSACTION HEADING { $CUSIP = uc('CUS' . $WORDS[$a+3]); # Read CUSIP code # print "\n$CUSIP"; $#WORDS = $a; # Remove '/' and whatever comes after it $NAME = join(' ', @WORDS); # What's left is the company name $TYPE = $STRIKE = $EXP_DATE = $DATE_OPEN = $DATE_CLOSE = $QTY = $COST = $SOLD = $WASH_SALE = $PROFIT = ''; $SYMBOL = GetSymbol_or_CUSIP($CUSIP); } elsif (Find($PATTERN, 'AWDWPFWFW')) # OPTION TRANSACTION HEADER { ($SYMBOL, $EXP_DATE, $TYPE, $STRIKE) = @WORDS; $W = $NAME = $QTY = $DATE_OPEN = $DATE_CLOSE = $COST = $SOLD = $PROFIT = ''; } elsif (Find($PATTERN, 'ADPPDPPP')) # OPTION TRANSACTION { $W = ''; ($DATE_CLOSE, $QTY, $SOLD, $DATE_OPEN, $COST, $WASH_SALE, $PROFIT) = @WORDS; SaveTransaction(); } elsif (Find($PATTERN, 'ADPPDPPWP')) # OPTION TRANSACTION { ($DATE_CLOSE, $QTY, $SOLD, $DATE_OPEN, $COST, $WASH_SALE, $W, $PROFIT) = @WORDS; SaveTransaction(); } elsif (Find($PATTERN, 'ADPPDPPWPW')) # STOCK TRANSACTION { if (uc($WORDS[8]) eq 'TOTAL') # Only save totals { ($DATE_CLOSE, $QTY, $SOLD, $WASH_SALE, $COST, $DATE_OPEN, $COST, $WASH_SALE, $W, $PROFIT) = @WORDS; SaveTransaction(); } } elsif (Find($PATTERN, 'AWWPPPP')) # SECURITY TOTALS { if (Find($RAW_DATA, 'GRAND TOTAL')) # Save grand total for later { ($W, $W, $SOLD, $COST, $W, $WASH_SALE, $PROFIT) = @WORDS; @GRAND_TOTAL = ($SOLD, $COST, $WASH_SALE, $PROFIT); } $CUSIP = $NAME = $SYMBOL = $W = $QTY = $TYPE = $DATE_OPEN = $DATE_CLOSE = $EXP_DATE = $STRIKE = $COST = $SOLD = $WASH_SALE = $PROFIT = ''; } # Note: Sometimes if you sell 500 shares, Robinhood will # sell 50 shares at some price, then 250 shares later at a # different price, and so eventually you end up with a # bunch of little sales which each take up a separate line. # We don't want that. We only record sale totals. if ($W ne 'W') { $WASH_SALE = 0; } $TYPE =~ tr|a-z|A-Z|; $TYPE =~ tr|a-zA-Z||cd; } # Create footer my $LINE_COUNT = CountStr($OUTPUT, "\n"); # Count number of lines # Add calculated totals $COST = $SOLD = $WASH_SALE = $PROFIT = "=SUM(X2:X$LINE_COUNT)"; my $COLUMN; $COLUMN = FindColumn('COST'); $COST =~ s/X/$COLUMN/g; $COLUMN = FindColumn('SOLD'); $SOLD =~ s/X/$COLUMN/g; $COLUMN = FindColumn('PROFIT'); $PROFIT =~ s/X/$COLUMN/g; $COLUMN = FindColumn('WASH_SALE'); $WASH_SALE =~ s/X/$COLUMN/g; $DESCRIPTION = 'CALCULATED TOTAL >'; $NAME = $QTY = $TYPE = $SYMBOL = $CUSIP = $PATTERN = $DATE_OPEN = $DATE_CLOSE = $STRIKE = $EXP_DATE = ''; $OUTPUT .= eval('"' . $HEADER . '"'); # Add grand total (last line) ($SOLD, $COST, $WASH_SALE, $PROFIT) = @GRAND_TOTAL; $DESCRIPTION = 'GRAND TOTAL >'; $OUTPUT .= eval('"' . $HEADER . '"'); # Save CSV file. $OUTPUT =~ tr/,//d; # Remove all commas that would mess up the columns $OUTPUT =~ tr/|/,/; # Change '|' back to commas CreateFile($OUTPUT_FILE, $OUTPUT); print " Created: $OUTPUT_FILE"; } ################################################## # This function saves a transaction. # Usage: SaveTransaction() # sub SaveTransaction { $NO++; if ($WASH_SALE eq '...') { $WASH_SALE = '0'; } $EXP_DATE = FormatDate($EXP_DATE); $DATE_OPEN = FormatDate($DATE_OPEN); $DATE_CLOSE = FormatDate($DATE_CLOSE); my $OPTION = $TYPE =~ /PUT|CALL/; if ($OPTION) { $DESCRIPTION = ShortDate($EXP_DATE) . " $SYMBOL $STRIKE " . TitleCase($TYPE); } else { $DESCRIPTION = TruncateStr($NAME, 24) . " ($SYMBOL)"; } my $UNIT = ($OPTION) ? ($QTY > 1 ? 'contracts' : 'contract') : 'sh'; $DESCRIPTION = $QTY . ' ' . $UNIT . ' of ' . $DESCRIPTION; $DESCRIPTION =~ s/\s*([0-9,]*)[.0]*\s+/$1 /; $OUTPUT .= eval('"' . $HEADER . '"'); } ################################################## # # This function makes sure that any date that is # MM/DD/YY becomes MM/DD/YYYY by adding '19' or # '20' to it. Years 90-99 will be interpreted as # 1990...1999, while all other dates 2000...2089. # Usage: STRING = FormatDate(STRING) # sub FormatDate { my $DATE = defined $_[0] ? $_[0] : ''; my $L = length($DATE); $L == 8 or return $DATE; my $YR = substr($DATE, 6); $YR = ((vec($YR, 0, 8) > 56) ? '19' : '20') . $YR; return substr($DATE, 0, 6) . $YR; } ################################################## # v2021.3.2 # This function expects a list of words made up # of letters and/or numbers, etc. # It looks at each word and decides what it is. # If it looks like a date, then it writes 'D' # If it looks like a price, then it writes 'P' # If it's a 9-digit CUSIP code, then writes 'C' # If it's a forward slash, then it writes 'F' # If it looks like some English word or # something else, then it writes 'W' # The letter 'A' marks the beginning of each line. # As each word is analyzed, a unique pattern # will emerge, which will be returned to the caller. # # Usage: STRING = CreatePattern(ARRAY_OF_STRINGS) # sub CreatePattern { my $PATTERN = 'A'; foreach my $S (@_) # Look at each string one by one { if ($S =~ /Various|[01][0-9]\/[0-3][0-9]\/2[0123]+/) { $PATTERN .= 'D'; } elsif ($S =~ /[0-9A-Z]{9}/ && length($S) == 9) { $PATTERN .= 'C'; } elsif ($S =~ /[0-9,.\-\(\)]+/) { $PATTERN .= 'P'; } elsif ($S =~ /[a-zA-Z.:\&\+\-]/) { $PATTERN .= 'W'; } elsif ($S eq '/') { $PATTERN .= 'F'; } } return $PATTERN; } ################################################## # v2021.3.2 # This function returns 1 when substring is found # within string, or returns 0 if not found. # Also updates the value of global variable $a # as follows: When not found, $a will be zero. # When found, $a will point 2 characters before # the first occurrance of substring. # Usage: INTEGER = Find(STRING, SUBSTRING) # sub Find { $[ = 0; defined $_[0] or return 0; defined $_[1] or return 0; my $L = length($_[1]); $L <= length($_[0]) or return 0; my $PTR = index(uc($_[0]), uc($_[1])); $a = $PTR - 2; return $PTR < 0 ? 0 : 1; } ################################################## # # This function receives a 9-letter CUSIP code # and returns the stock symbol. If the symbol # is not found, then it will be left blank. # This function can also work backwards: when # given a stock symbol, it returns the CUSIP. # Usage: STRING = GetSymbol_or_CUSIP(STRING) # sub GetSymbol_or_CUSIP { my $S = defined $_[0] ? uc($_[0]) : ''; length($S) or return ''; # If we can find the CUSIP or symbol within the program, GREAT! $SYMBOL_LIST =~ / $S\=([0-9A-Z]+) /; if (defined $1) { return $1; } $SYMBOL_LIST =~ / CUS([0-9A-Z]+)\=$S /; if (defined $1) { return $1; } # But if we cannot find it, then we need to search the big database. if (length($CUSIP_DB) == 0) { $CUSIP_DB = ReadFile($CUSIP_FILE); } # This should be about 200KB $CUSIP_DB =~ /\n([A-Z]+),$S,/; if (defined $1) { return $1; } $CUSIP_DB =~ /\n$S,CUS([0-9A-Z]+),/; if (defined $1) { return $1; } return ''; } ################################################## # v2019.11.23 # Counts how many times SUBSTR occurs in STRING and # returns the number. The search is case sensitive. # Usage: INTEGER = CountStr(STRING, SUBSTR) # sub CountStr { defined $_[0] or return 0; defined $_[1] or return 0; (my $LA = length($_[0])) or return 0; (my $LB = length($_[1])) or return 0; $LA >= $LB or return 0; my $COUNT = 0; for (my $i = 0; $i < $LA; $i += $LB) { $i = index($_[0], $_[1], $i); $i >= 0 or last; $COUNT++; } return $COUNT; } ################################################## # v2021.3.3 # This function cuts a text to a certain length # and adds '...' at the end if it was too long. # Usage: STRING = TruncateStr(STRING) # sub TruncateStr { my $S = defined $_[0] ? Trim($_[0]) : ''; my $MAXLEN = defined $_[1] ? $_[1] : 20; my $SUFFIX = '...'; $MAXLEN > 3 or return $SUFFIX; return ($MAXLEN > length($S)) ? $S : substr($S, 0, $MAXLEN - 3) . $SUFFIX; } ################################################## # v2021.3.3 # This function expects a date in MM/DD/YYYY format # and returns a date in MmmDD format. Example: # ShortDate("03/26/2020") --> "Mar26" # Usage: STRING = ShortDate(STRING) # sub ShortDate { my $DATE = defined $_[0] ? $_[0] : ''; $DATE =~ /([01]*[0-9])\/([0-3]*[0-9])[\/0-9]*/; return (defined $1 && defined $2) ? substr($MONTHS, ($1 * 3), 3) . ($2 * 1) : ''; } ################################################## # v2021.3.3 # Capitalizes the first letter of every word. # Usage: ASCII_STRING = TitleCase(ASCII_STRING) # sub TitleCase { my $S = defined $_[0] ? lc($_[0]) : ''; my $L = length($S); my $LETTERCOUNT = 0; my $WORD_SEPARATORS = " .,:;!?&/\()[]{}<>|-+=\t\n\r\xFF"; for (my $i = 0; $i < $L; $i++) { if (index($WORD_SEPARATORS, substr($S, $i, 1)) >= 0) { $LETTERCOUNT = 0; } elsif ($LETTERCOUNT++ == 0) { my $c = vec($S, $i, 8); if ($c > 96 && $c < 123) # Convert letter to upper case { vec($S, $i, 8) = $c & 223; } } } return $S; } ################################################## # # This function returns the appropriate Excel column # when given a heading label. It does this by looking # for the header label in the $HEADER global variable. # Example: FindColumn('COST') --> 'D' # Usage: STRING = FindColumn(LABEL) # sub FindColumn { my $LABEL = defined $_[0] ? $_[0] : ''; length($LABEL) or return ''; my $P = index(uc($HEADER), uc($LABEL)); return ($P < 0) ? '' : chr(65 + CountStr(substr($HEADER, 0, $P), '|')); } ################################################## # v2019.11.24 # Creates and overwrites a file in binary mode. # Returns 1 on success or 0 if something went wrong. # Usage: INTEGER = CreateFile(FILE_NAME, CONTENT) # sub CreateFile { defined $_[0] or return 0; my $F = $_[0]; $F =~ tr/\"\0*?|<>//d; # Remove special characters length($F) or return 0; local *FH; open(FH, ">$F") or return 0; binmode FH; if (defined $_[1] ? length($_[1]) : 0) { print FH $_[1]; } close FH or return 0; return 1; } ################################################## # v2021.3.1 # This function joins two or more paths and returns # a complete path string in localized format. # Usage: STRING = FormatPath(STRINGs...) # sub FormatPath { my $P = ''; foreach (@_) # Trim SPACEs, double quotes, TAB, CR, LF, etc. { $P .= '/' . TrimChar($_, " \"\t\r\n\0\f"); } $P = substr($P, 1); $P =~ tr|\\|/|; # Convert to Linux format $P =~ tr|/||s; # Remove duplicate '//' $P =~ s/\/[^\/]+\/\.\.//; # Resolve '/directory_name/..' if ($^O =~ /DOS|WIN/i) { $P =~ tr|/|\\|; } # Convert to DOS format return $P; } ################################################## # Prints the description of this program. # Usage: About() # sub About { my $PTRSIZE = `$^X -V:ptrsize`; $PTRSIZE =~ s/[^0-9]//g; print "\nPerl $] ", ($PTRSIZE << 3), '-bit ', $^O, ' ' x 20, TimeStamp(), "\n\n$0"; my $S = ReadFile($0, 0, 1000); my $P = 1 + index($S, '# '); my $E = 1 + index($S, '###', $P); $P && $E or return; $S = substr($S, $P, $E - $P); $S =~ tr|#| |; print "\n\n ", RTRIM($S); } ################################################## # # This function reads the entire contents of a file # in binary mode and returns it as a string. If an # errors occur, an empty string is returned silently. # A second argument will move the file pointer before # reading. And a third argument limits the number # of bytes to read. # Usage: STRING = ReadFile(FILENAME, [START, [LENGTH]]) # sub ReadFile { my $NAME = defined $_[0] ? $_[0] : ''; $NAME =~ tr/\"\0*?|<>//d; # Remove special characters -e $NAME or return ''; -f $NAME or return ''; my $SIZE = -s $NAME; $SIZE or return ''; my $LEN = defined $_[2] ? $_[2] : $SIZE; $LEN > 0 or return ''; local *FH; sysopen(FH, $NAME, 0) or return ''; binmode FH; my $POS = defined $_[1] ? $_[1] : 0; $POS < $SIZE or return ''; $POS < 1 or sysseek(FH, 0, $POS); # Move file ptr my $DATA = ''; sysread(FH, $DATA, $LEN); # Read file close FH; return $DATA; } ################################################## # # This function reads the contents of a folder # and returns an array that contains file names # whose extensions match the ones specified in # the second argument. The second argument should # be a string containing extensions separated # by a single space. # Example: ReadDIR('/work/text', 'TXT TEXT HTM') # # Usage: ARRAY = ReadDIR(PATH, [EXTENSIONS]) # sub ReadDIR { my @FILELIST; my $PATH = defined $_[0] ? $_[0] : ''; my $FILTER = defined $_[1] ? $_[1] : ''; my $F = length($FILTER); length($PATH) or return @FILELIST; $PATH .= '/'; # Make sure that path ends with '/' $PATH =~ tr|\\|/|; # Convert path to Linux format $PATH =~ tr|/||s; # Remove double '//' if ($F) { # Format filter $FILTER =~ tr|a-z A-Z 0-9 _ ||cd; # Remove bad characters $FILTER =~ tr|a-z|A-Z|; # Convert to uppercase $FILTER = " $FILTER "; # Add spaces for easy search } my $FULLNAME; local *DIR; opendir(DIR, $PATH) or return @FILELIST; while ((my $NAME = readdir(DIR))) { if (length($NAME) < 3) { $NAME ne '.' && $NAME ne '..' or next; } my $FULLNAME = "$PATH$NAME"; # Ignore subdirectories; just deal with files. if (-f($FULLNAME)) { # Do we return all files or just the ones # that have a certain extension? if ($F) { # Grab the file extension my $P = rindex($NAME, '.'); $P++ > 0 or next; my $EXT = uc(substr($NAME, $P)); # Skip file if its extension doesn't match index($FILTER, " $EXT ") >= 0 or next; } push(@FILELIST, $NAME); # Add file to the list } } closedir(DIR); return @FILELIST; } ################################################## # # This function removes all whitespace from before # and after text and returns a new string. This # function removes every character whose ASCII # value is less than 33. This includes tab, space, # null, vertical tab, esc, new lines, etc.. # # Usage: STRING = Trim(STRING) # sub Trim { my $S = defined $_[0] ? $_[0] : ''; my $L = length($S) or return ''; my $START = 0; my $LAST = 0; while ($L--) { if (vec($S, $L, 8) > 32) { $START = $L; $LAST or $LAST = $L + 1; } } return substr($S, $START, $LAST - $START); } ################################################## # v2019.6.15 # This function is just like Trim() except it # removes characters specified in SUBSTR. # Usage: STRING = TrimChar(STRING, SUBSTR) # sub TrimChar { defined $_[0] or return ''; my $L = length($_[0]); $L or return ''; defined $_[1] or return $_[0]; length($_[1]) or return $_[0]; my $START = 0; my $LAST = 0; while ($L--) { if (index($_[1], substr($_[0], $L, 1)) < 0) { $START = $L; $LAST or $LAST = $L + 1; } } return substr($_[0], $START, $LAST - $START); } ################################################## # Just like the RTRIM$() function in BASIC language, # this function removes whitespace from the right # side of a string. # Usage: STRING = RTRIM(STRING) sub RTRIM { defined $_[0] or return ''; my $L = length($_[0]); while (vec($_[0], --$L, 8) < 33) {} return substr($_[0], 0, $L + 1); } ################################################## # v2021.2.21 # Returns the current working directory. # Usage: STRING = GetCurrentWorkingDirectory() # sub GetCurrentWorkingDirectory { return FormatPath($^O =~ /WIN|DOS/i ? `CD` : (exists($ENV{PWD}) ? $ENV{PWD} : `pwd`)); } ################################################## # This function returns the current date and time # in the following format: Mmm D YYYY HH:MM:SSmm # Usage: STRING = TimeStamp() # sub TimeStamp { my @D = localtime(); my $M = substr($MONTHS, $D[4] * 3, 3); my $A = $D[2] > 11 ? 'pm' : 'am'; $D[2] or $D[2] = 12; $D[2] < 13 or $D[2] -= 12; return sprintf("%s %d %d %d:%.02d:%.02d%s", $M, $D[3], 1900+$D[5], $D[2], $D[1], $D[0], $A); } ################################################## sub PAUSE { # Wait for user to press Enter... $| = 1; print "\n\nPRESS TO EXIT..."; $a = ; } ################################################## sub PrintDottedLine { print "\n\n", '.' x 66, "\n\n"; } ##################################################