Skip to content

Instantly share code, notes, and snippets.

@mattvonrocketstein
Created October 8, 2017 06:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattvonrocketstein/4c1a573015fcdc7502b05a65eeec6265 to your computer and use it in GitHub Desktop.
Save mattvonrocketstein/4c1a573015fcdc7502b05a65eeec6265 to your computer and use it in GitHub Desktop.
proto.exs
#
# 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