I've been asked to write a decompressor library for an interview process. It's a LZW decompression library. I've got it working and tried it out. I'm looking for any feedback on structure, presentation, readability from professionals who've worked as software engineers.
LZW explanation. The second table in uncompression section explains how the dictionary is built and key size is limited to 12-bits in my case.
"""
Decompressor library for Lempel–Ziv–Welch compression algorithm.
Main Functions:
decompress(file_path)
extract_archive_to_file(input_path, output_path)
Test code in the end.
"""
def read_compressed_codes_from_archive(file_path: str) -> [int]:
"""
Reads file at specified path and returns content as an int array.
:param file_path: path to archive
:return: array of int codes
"""
compressed_codes = []
with open(file_path, "rb") as archive:
# Read the 3 bytes, which contain two 12-bit codes
bytes = archive.read(3)
while bytes:
if len(bytes) == 3:
code1, code2 = unpack_codes_from_3_bytes(bytes)
compressed_codes.append(code1)
compressed_codes.append(code2)
elif len(bytes) == 2:
# Odd number of codes in the end of the file
# Extract a single 12-bit code which is padded to 16-bits
code = (0b0000111111111111 & (bytes[0] << 8)) | bytes[1]
compressed_codes.append(code)
bytes = archive.read(3)
return compressed_codes
def unpack_codes_from_3_bytes(packed_bytes: [bytes]) -> (int, int):
"""
Extracts two 12-bit numbers from 3 bytes (24-bits)
:param packed_bytes: array of 3 int bytes
:return: extracted int codes
"""
# Create 16-bit int vars to store extracted codes in
code1 = 0b0000111111111111
code2 = 0b0000111111111111
# Copy over 8 most significant bits and shift them to correct place
code1 = code1 & packed_bytes[0] << 4
# Copy over the 4 least significant bits from second byte
code1 = code1 | (packed_bytes[1] >> 4)
# Copy over the 4 most significant bits from second byte
code2 = code2 & packed_bytes[1] << 8
# Copy over the 8 least significant bits from third byte
code2 = code2 | packed_bytes[2]
return code1, code2
def convert_codes_to_text(compressed_codes: [int]) -> str:
"""
Decompresses codes into text data.
:param compressed_codes: compressed codes from archive
:return: string data from compressed codes
"""
# Create initial dictionary with individual char compressed_codes
dictionary_size = 256
dictionary = {i: chr(i) for i in range(dictionary_size)}
text_data = ""
previous_code = None
# Loop over compressed compressed_codes and convert them to text values.
for code in compressed_codes:
if code in dictionary:
text_data += dictionary[code]
if previous_code:
# Make sure new value is not already in the dict,
# otherwise add it to the end.
if dictionary[previous_code] + dictionary[code][0] not in \
dictionary.keys():
dictionary[dictionary_size] = dictionary[previous_code] \
+ dictionary[code][0]
dictionary_size += 1
# If dictionary is full, reset back to initial dict
# That is 4096 in binary ie one and 12 zeros
if dictionary_size == 0b1000000000000:
dictionary_size = 256
else:
assert ("Decompression error. Code {} not present in "
"dictionary".format(code))
previous_code = code
return text_data
def decompress(file_path: str) -> str:
"""
Decompress an archive and get the text data.
:param filepath: path to archive
:return: text data as string
"""
codes = read_compressed_codes_from_archive(file_path)
text_data = convert_codes_to_text(codes)
return text_data
def extract_archive_to_file(input_path: str, output_path: str):
"""
Extracts an archive and saves it on disk.
:param input_path: path to archive
:param output_path: path to extracted file
:return: none
"""
text_data = decompress(input_path)
with open(output_path, "w") as output_file:
output_file.write(text_data)
def test_library():
"""
Example test function.
"""
file_path = "LzwInputData/compressedfile3.z"
text_data = decompress(file_path)
print(text_data)
output_path = "LzwInputData/compressedfile3.txt"
extract_archive_to_file(file_path, output_path)
test_library()
Thanks