Python date compression

I m making a script to store date information in the smallest size in bits

(I don't mind of the actually memory use in the program / runtime)

#!/usr/bin/env python
import random
import datetime

MAGIC = "DAT"

def extract(data, size):
return (((data >> size) << size) ^ data)

def compress_date(year, month, day, hour, minute):
assert year <= 31
assert month <= 12
assert day <= 31
assert hour <= 24
assert minute <= 60

data = 0
data = (data << 5) + year
data = (data << 4) + month
data = (data << 5) + day
data = (data << 5) + hour
data = (data << 6) + minute
return (data)

def decompress_date(data):
minute = extract(data, 6)
data >>= 6

hour = extract(data, 5)
data >>= 5

day = extract(data, 5)
data >>= 5

month = extract(data, 4)
data >>= 4

year = extract(data, 5)
data >>= 5
return (year, month, day, hour, minute)

date = serialgen.compress_date(10, 11, 30, 24, 60)
print date
print hex(date)
print decompress_date(date)


output:

11269692
0xabf63c
(10, 11, 30, 24, 60)


Can I do something to clear up the ugly parts:

data = 0
data = (data << 5) + year
data = (data << 4) + month
data = (data << 5) + day
data = (data << 5) + hour
data = (data << 6) + minute

minute = extract(data, 6)
data >>= 6

hour = extract(data, 5)
data >>= 5

day = extract(data, 5)
data >>= 5

month = extract(data, 4)
data >>= 4

year = extract(data, 5)
data >>= 5


Do you have any suggestions to improve this code?

Weird limits

I have absolutely no idea of what date is "the 12th of June at 24 hour 60 minutes", yet you consider it a valid date as regard to your bounds. You should assert hours and minutes to be respectively 23 and 59 at max.

Using a two-digit long value for the date seems rather useless too. What century does it encode exactly? And assuming it is for years in the 2000 era, why limit to 2031?

Unused stuff

Both your imports and constant are unused, you should remove them.

Bitfield manipulation

Extracting informations out of a bitfield is generally done using a mask. You need to create the mask corresponding to size bits set using mask = (1 << (size + 1)) - 1 and then you can use data & mask to get the desired value. The advantage being that you can easily add an offset to grab the exact portion you’re interested in:

def extract(data, size, offset):
mask = (1 << (size + 1)) - 1
return (data & (mask << offset)) >> offset

def decompress_date(data):
minute = extract(data, 6, 0)  # minutes are at the end, no offset
hour = extract(data, 5, 6)  # hours are after minutes (6)
day = extract(data, 5, 11)  # days are after hours (5) and minutes (6)
month = extract(data, 4, 16)  # you get it
year = extract(data, 5, 20)
return (year, month, day, hour, minute)


Value creation

You can simplify the compressed data creation by dropping useless initialization:

def compress(...):
data = year
data = (data << 4) + month
...


Knowing the offsets, you can also use them to create the compressed data all at once. To simplify reading and understanding, you can also define a bunch of constants:

MINUTES_SIZE = 6
HOURS_SIZE = 5
DAYS_SIZE = 5
MONTHS_SIZE = 4
YEARS_SIZE = 5

MINUTES_OFFSET = 0
HOURS_OFFSET = MINUTES_OFFSET + MINUTES_SIZE
DAYS_OFFSET = HOURS_OFFSET + HOURS_SIZE
MONTHS_OFFSET = DAYS_OFFSET + DAYS_SIZE
YEARS_OFFSET = MONTHS_OFFSET + MONTHS_SIZE

def decompress_date(data):
minute = extract(data, MINUTES_SIZE, MINUTES_OFFSET)
hour = extract(data, HOURS_SIZE, HOURS_OFFSET)
day = extract(data, DAYS_SIZE, DAYS_OFFSET)
month = extract(data, MONTHS_SIZE, MONTHS_OFFSET)
year = extract(data, YEARS_SIZE, YEARS_OFFSET)
return (year, month, day, hour, minute)

def compress_date(year, month, day, hour, minute):
assert year <= 31
assert 0 < month <= 12
assert 0 < day <= 31
assert 0 <= hour < 24
assert 0 <= minute < 60

return (
(year << YEARS_OFFSET) +
(month << MONTHS_OFFSET) +
(day << DAYS_OFFSET) +
(hour << HOURS_OFFSET) +
(minute << MINUTES_OFFSET)
)


Timestamps

The "classic" condensed representation of a point in time is given by a timestamp: the elapsed time in seconds since January 1st 1970. Since you’re only interested in minutes, you can create a simple wrapper around Python's datetime.timestamp():

import datetime

def compress(date):
return int(date.timestamp() // 60)

def compressed_date(year, month, day, hour, minute):
return compress_date(datetime.datetime(year, month, day, hour, minute)

def uncompress(truncated_timestamp):
return datetime.datetime.fromtimestamp(truncated_timestamp * 60)


And support a wider variety of dates.