Module: MppReader::Decode

Defined in:
lib/mpp_reader/decode.rb

Overview

Decoders for MS Project’s binary value encodings, ported from MPXJ MPPUtility. The epoch is 1983-12-31; dates are u16 days since the epoch; times of day and durations are stored in tenths of a minute; timestamps combine a u16 time (6-second units) with a u16 day count.

Constant Summary collapse

EPOCH_DATE =
Date.new(1983, 12, 31)
DURATION_UNITS_MASK =
0x1F
DURATION_UNITS =
{
  3 => :minutes,
  4 => :elapsed_minutes,
  5 => :hours,
  6 => :elapsed_hours,
  7 => :days,
  8 => :elapsed_days,
  9 => :weeks,
  10 => :elapsed_weeks,
  11 => :months,
  12 => :elapsed_months,
  19 => :percent,
  20 => :elapsed_percent
}.freeze
TENTHS_PER_UNIT =

Divisors converting tenths-of-minutes to each unit, assuming MS Project’s defaults (8h days, 40h weeks, 20-day months).

{
  minutes: 10, elapsed_minutes: 10,
  hours: 600, elapsed_hours: 600,
  days: 4800, elapsed_days: 14_400,
  weeks: 24_000, elapsed_weeks: 100_800,
  months: 96_000, elapsed_months: 432_000
}.freeze

Class Method Summary collapse

Class Method Details

.adjusted_duration(value, units, minutes_per_day:, minutes_per_week:, days_per_month:) ⇒ Object

Like duration, but day/week/month conversions honour the project’s configured working time instead of the defaults.



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/mpp_reader/decode.rb', line 87

def adjusted_duration(value, units, minutes_per_day:, minutes_per_week:, days_per_month:)
  tenths_per_unit =
    case units
    when :days then minutes_per_day * 10
    when :weeks then minutes_per_week * 10
    when :months then minutes_per_day * days_per_month * 10
    end
  if tenths_per_unit.nil? || tenths_per_unit.zero?
    duration(value, units)
  else
    Duration.new(value.to_f / tenths_per_unit, units)
  end
end

.date(data, offset) ⇒ Object



43
44
45
46
47
48
# File 'lib/mpp_reader/decode.rb', line 43

def date(data, offset)
  days = data.byteslice(offset, 2).to_s.unpack1("v")
  return nil if days.nil? || days == 0xFFFF

  EPOCH_DATE + days
end

.duration(value, units) ⇒ Object

value is in tenths of a minute; converts using MS Project’s default hours-per-day assumptions.



81
82
83
# File 'lib/mpp_reader/decode.rb', line 81

def duration(value, units)
  Duration.new(value.to_f / TENTHS_PER_UNIT.fetch(units, 1), units)
end

.duration_units(type, default: :days) ⇒ Object



72
73
74
75
76
77
# File 'lib/mpp_reader/decode.rb', line 72

def duration_units(type, default: :days)
  code = type & DURATION_UNITS_MASK
  return default if code == 21

  DURATION_UNITS.fetch(code, :days)
end

.timestamp(data, offset) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/mpp_reader/decode.rb', line 50

def timestamp(data, offset)
  days = data.byteslice(offset + 2, 2).to_s.unpack1("v")
  return nil if days.nil? || days <= 1 || days == 0xFFFF

  time = data.byteslice(offset, 2).unpack1("v")
  time = 0 if time == 0xFFFF
  seconds = time * 6
  # Very small day counts show as NA in MS Project; a non-zero seconds
  # component distinguishes NA from real values (MPXJ heuristic).
  return nil if days < 100 && (seconds % 60) != 0

  to_time(EPOCH_DATE + days, seconds)
end

.timestamp_from_tenths(data, offset) ⇒ Object



64
65
66
67
68
69
70
# File 'lib/mpp_reader/decode.rb', line 64

def timestamp_from_tenths(data, offset)
  tenths = data.byteslice(offset, 4).to_s.unpack1("V")
  return nil if tenths.nil?

  seconds = tenths * 6
  to_time(EPOCH_DATE + (seconds / 86_400), seconds % 86_400)
end

.to_time(date, seconds_of_day) ⇒ Object



101
102
103
# File 'lib/mpp_reader/decode.rb', line 101

def to_time(date, seconds_of_day)
  Time.new(date.year, date.month, date.day) + seconds_of_day
end