Created
October 8, 2017 06:35
-
-
Save mattvonrocketstein/4c1a573015fcdc7502b05a65eeec6265 to your computer and use it in GitHub Desktop.
proto.exs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Running this file: | |
# | |
# Download txnlog.dat: | |
# curl -L -o txnlog.dat https://github.com/adhocteam/homework/blob/6d5d1c71069758/proto/txnlog.dat?raw=true | |
# | |
# Run this file with your own elixir stack: | |
# elixir proto.exs | |
# | |
# Or, use the official elixir stack for docker: | |
# docker run -v $PWD:/workspace --workdir /workspace elixir:1.4.5 elixir proto.exs | |
# | |
# See COMMENTS.md for more information | |
# | |
# datastructure that maps enum_from_protocol_spec to | |
# a tuple of [friendly_name, cash_transaction_bool] | |
record_types = %{ | |
0 => [:credit, true] , | |
1 => [:debit, true] , | |
2 => [:start_auto, false], | |
3 => [:stop_auto, false] } | |
# helper function for displaying data | |
show = fn (msg, val) -> | |
IO.puts("#{msg} #{val}") | |
end | |
# function that pops the header off the binary | |
# stream, returning `{header, rest_of_stream}`. | |
# | |
# the header spec is: | |
# * 4 byte magic string "MPS7" | |
# * 1 byte version, | |
# * 4 byte (uint32) for number of records | |
read_header = fn (data) -> | |
<< "MPS7", | |
version::size(8), | |
num_records::unsigned-integer-size(32), | |
rest_of_stream :: binary >> = data | |
header = %{ | |
version: version, | |
num_records: num_records } | |
{header, rest_of_stream} | |
end | |
# function that pops the next record off the binary | |
# stream, returning {record, rest_of_stream} | |
pop_record = fn records -> | |
<< record_type::size(8), | |
timestamp::unsigned-integer-size(32), | |
uid::unsigned-integer-size(64), | |
rest_of_stream::binary >> = records | |
# pop dollar value off the stream, maybe, depending on record type | |
[type_name, has_dollars] = record_types[record_type] | |
{dollars, rest_of_stream} = if has_dollars do | |
<< dollars::float-size(64), updated_stream::binary >> = rest_of_stream | |
{dollars, updated_stream} | |
else | |
{0, rest_of_stream} | |
end | |
record = %{ | |
uid: uid, | |
timestamp: timestamp, | |
record_type: type_name, | |
dollars: dollars, } | |
{record, rest_of_stream} | |
end | |
# function that slurps the entire binary file. this implementation is naive.. | |
# a different approach would be better for extremely large files | |
read_file = fn fname -> | |
{:ok, file} = File.open(fname) | |
IO.binread(file, :all) | |
end | |
# function that filters a group of records for the given user id | |
get_user_records = fn (records, user) -> | |
Enum.filter(records, fn record -> record[:uid]==user end) | |
end | |
# function that parses `num_records` records into elixir datastrcutures | |
# from the given binary data. this works by calling `pop_record` repeatedly | |
parse_records = fn (all_records, num_records) -> | |
tmp = Enum.reduce( | |
# First argument to `reduce` is just a sequence we use for the counter. | |
# Note that we take the header seriously and only read as many | |
# records as that implies. We'll deal with situations like bad headers | |
# and extra records later | |
0..num_records, | |
# Second argument to `reduce` initializes the accumulator data structure | |
%{remaining_data: all_records, parsed_data: []}, | |
# Third argument to `reduce` describes how to update the accumulator | |
fn(index, acc) -> | |
{record, rest} = pop_record.(acc[:remaining_data]) | |
IO.inspect ["record #{index}", record] | |
parsed_data = [record | acc[:parsed_data]] | |
%{remaining_data: rest, parsed_data: parsed_data} | |
end) | |
# Map was for readability in this function; | |
# we flatten results for callers | |
{tmp[:remaining_data], tmp[:parsed_data]} | |
end | |
# function that computes overall statistics for given records | |
compute_stats = fn records -> | |
Enum.reduce( | |
# First argument to reduce is the complete list of parsed records | |
records, | |
# Second argument to `reduce` initializes the accumulator data structure | |
%{ | |
dollars_debit: 0.0, | |
dollars_credit: 0.0, | |
autopay_start: 0, | |
autopay_end: 0, | |
}, | |
# Third argument to `reduce` is a function, which is applied | |
# to each record and describes how to update the accumulator | |
fn(record, acc) -> | |
# 4 ternaries derive the values we'll use to increment the accumulator | |
credit_dollars = if record[:record_type]==:credit, do: record[:dollars], else: 0 | |
debit_dollars = if record[:record_type]==:debit, do: record[:dollars], else: 0 | |
autopay_start_increment = if record[:record_type]==:start_auto, do: 1, else: 0 | |
autopay_stop_increment = if record[:record_type]==:stop_auto, do: 1, else: 0 | |
%{ | |
dollars_debit: acc[:dollars_debit] + debit_dollars, | |
dollars_credit: acc[:dollars_credit] + credit_dollars, | |
autopay_start: acc[:autopay_start] + autopay_start_increment, | |
autopay_end: acc[:autopay_end] + autopay_stop_increment, | |
} | |
end) | |
end | |
# function that returns the sum of all transaction records for the given user | |
get_user_balance = fn records, user -> | |
Enum.reduce( | |
get_user_records.(records, user), | |
0, | |
fn record, acc -> | |
acc + record[:dollars] | |
end) | |
end | |
# function to display the results of aggregate statistics | |
show_stats = fn records -> | |
IO.puts("\ncomputing stats:\n") | |
stats = compute_stats.(records) | |
show.("Autopays started?\t\t\t", stats[:autopay_start]) | |
show.("Autopays ended?\t\t\t\t", stats[:autopay_end]) | |
show.("Total amount in dollars of debits?\t", stats[:dollars_debit]) | |
show.("Total amount in dollars of credits?\t", stats[:dollars_credit]) | |
end | |
# function to show the balance for the given user | |
show_user_balance = fn records, user -> | |
user_balance = get_user_balance.(records,user) | |
show.("Balance of user (ID #{user})?", user_balance) | |
end | |
# Entrypoint | |
main = fn -> | |
all_data = read_file.("txnlog.dat") | |
{header, all_records} = read_header.(all_data) | |
IO.inspect([:header, header]) | |
{extra_data, records} = parse_records.(all_records, header[:num_records]) | |
IO.puts("\ndone parsing.") | |
IO.puts("\nextra data (this is empty when header is correct): #{extra_data}") | |
magic_user = 2456938384156277127 | |
show_stats.(records) | |
show_user_balance.(records, magic_user) | |
end | |
main.() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment