Skip to content

Commit

Permalink
Merge 0d06bae into b790f19
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Worek committed Jul 3, 2017
2 parents b790f19 + 0d06bae commit f5059ab
Show file tree
Hide file tree
Showing 20 changed files with 656 additions and 330 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Expand Up @@ -2,3 +2,9 @@ coverage/
doc
.yardoc/
pkg/

# ctags
tags

# vim
*.swp
6 changes: 3 additions & 3 deletions Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
merch_calendar (0.0.5)
merch_calendar (0.1.0.rc3)

GEM
remote: https://www.rubygems.org/
Expand All @@ -17,7 +17,7 @@ GEM
docile (1.1.5)
json (1.8.3)
method_source (0.8.2)
pry (0.10.3)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
Expand Down Expand Up @@ -58,4 +58,4 @@ DEPENDENCIES
simplecov

BUNDLED WITH
1.15.0
1.15.1
113 changes: 53 additions & 60 deletions README.md
Expand Up @@ -18,16 +18,6 @@ Add the following line to your `Gemfile`:
gem "merch_calendar"
```


## Configuration
```ruby
# NOTE: Configuration will be added soon, but is currently NOT available.
MerchCalendar.configure do |config|
# The month that Q1 begins. The default is 8 (August)
config.quarter_start_month = 8
end
```

## Usage

For converting a date into a `MerchWeek` object.
Expand Down Expand Up @@ -59,82 +49,85 @@ puts merch_week.to_s(:long) # "2013:48 Dec W5"
puts merch_week.to_s(:elasticsearch) # "2013-12w05"
```

This can also be used on the `MerchCalendar` module. All `start_` and `end_` methods can be called, along with a few additional ones.

```ruby
# All examples below return a Date object for the start of May within the 2014 merch year
MerchCalendar.start_of_month(2014, 5)
MerchCalendar.start_of_month(2014, month: 5)
MerchCalendar.start_of_month(2014, julian_month: 5)

# This is the same as May, because "Merch" months are shifted by 1.
# i.e. month 1 is actually February
# You probably will never use this, but it is available.
MerchCalendar.start_of_month(2014, merch_month: 4)
```

#### Confusing things to look out for:
### Merch retail calendar

Merch calendars have the first month in February, and the last (12th) month is in January of the following year. In the code block above, each method is *asking* a very different question. This will definitely cause confusion, so here are some explanations.
Merch calendars have their first month in February, and the last (12th) month is in January of the
following year.

```ruby
# This is asking "In the Merch year of 2014, where is the month of January?"
# January is the last (12th) month of a merch year, so this date will be in the NEXT
# julian calendar year
MerchCalendar.start_of_month(2014, 1)
MerchCalendar.start_of_month(2014, month: 1)
MerchCalendar.start_of_month(2014, julian_month: 1)
# => 2015-01-04
# ^^^^ - NEXT year
# This is asking "In the Merch year of 2014, what is the Gregorian calendar date of
# the start of the first month?"
retail_calendar = MerchCalendar::RetailCalendar.new

# This is asking "When is the start of the FIRST month of the merch year 2014"
MerchCalendar.start_of_month(2014, merch_month: 1)
retail_calendar.start_of_month(2014, 1)
# => 2014-02-02

retail_calendar.start_of_month(2014, 12)
# => 2015-01-04
```

This table should describe the progression of dates:

| N | `start_of_month(2014, N)` | `start_of_month(2014, merch_month: N)` |
| ------------- | ------------- | ------------- |
| 1 | **2015-01-04** | 2014-02-02 |
| 2 | 2014-02-02 | 2014-03-02 |
| 3 | 2014-03-02 | 2014-04-06 |
| 4 | 2014-04-06 | 2014-05-04 |
| 5 | 2014-05-04 | 2014-06-01 |
| 6 | 2014-06-01 | 2014-07-06 |
| 7 | 2014-07-06 | 2014-08-03 |
| 8 | 2014-08-03 | 2014-08-31 |
| 9 | 2014-08-31 | 2014-10-05 |
| 10 | 2014-10-05 | 2014-11-02 |
| 11 | 2014-11-02 | 2014-11-30 |
| 12 | 2014-11-30 | **2015-01-04** |
| N | `start_of_month(2014, N)` |
| ------------- | ------------- |
| 1 | 2014-02-02 |
| 2 | 2014-03-02 |
| 3 | 2014-04-06 |
| 4 | 2014-05-04 |
| 5 | 2014-06-01 |
| 6 | 2014-07-06 |
| 7 | 2014-08-03 |
| 8 | 2014-08-31 |
| 9 | 2014-10-05 |
| 10 | 2014-11-02 |
| 11 | 2014-11-30 |
| 12 | 2015-01-04 |


Other useful methods:

```ruby
# 52 or 53 (depending on leap year)
MerchCalendar.weeks_in_year(2015)
retail_calendar.weeks_in_year(2016)
# => 52
retail_calendar.weeks_in_year(2017)
# => 53

# get the start date of a given merch week
retail_calendar.start_of_week(2017, 4, 1)
# => #<Date: 2017-04-30 ((2457874j,0s,0n),+0s,2299161j)>

# get the end date of a given merch week
retail_calendar.end_of_week(2017, 4, 1)
#=> #<Date: 2017-05-06 ((2457880j,0s,0n),+0s,2299161j)>
```

### Offset fiscal year calendars
Some companies, one of which being Stitch Fix, operate on a fiscal year calendar that is offset of
the traditional retail calendar. The `MerchCalendar::FiscalYearCalendar` class allows you to easily
offset the start of year to match your fiscal calendar.

# returns an array of MerchWeek objects for each week within the provided month
MerchCalendar.weeks_for_month(2014, 1)
```ruby
fiscal_calendar = MerchCalendar::FiscalYearCalendar.new

# 52 or 53 (depending on leap year)
fiscal_calendar.weeks_in_year(2017)
# => 52
fiscal_calendar.weeks_in_year(2018)
# => 53

# get the start date of a given merch week
MerchCalendar.start_of_week(2017, 4, 1)
# => #<Date: 2017-04-02 ((2457846j,0s,0n),+0s,2299161j)>
fiscal_calendar.start_of_week(2017, 1, 1)
# => #<Date: 2016-07-31 ((2457601j,0s,0n),+0s,2299161j)>

# get the end date of a given merch week
MerchCalendar.end_of_week(2017, 4, 1)
# => #<Date: 2017-04-08 ((2457852j,0s,0n),+0s,2299161j)>
fiscal_calendar.end_of_week(2017, 4, 1)
#=> #<Date: 2017-05-06 ((2457880j,0s,0n),+0s,2299161j)>
```

## Documentation
You can view the documentation for this gem on [RubyDoc.info](http://www.rubydoc.info/github/stitchfix/merch_calendar/master).


## Roadmap
* Support for 4-4-5 calendars

## License
MerchCalendar is released under the [MIT License](http://www.opensource.org/licenses/MIT).
7 changes: 3 additions & 4 deletions lib/merch_calendar.rb
@@ -1,12 +1,11 @@

#
module MerchCalendar

DEPRECATION_DATE = Date.new(2018, 1, 1)
end

require_relative 'merch_calendar/version'
require_relative 'merch_calendar/util'
require_relative 'merch_calendar/configurable'
require_relative 'merch_calendar/configuration'
require_relative 'merch_calendar/merch_week'
require_relative 'merch_calendar/date_calculator'
require_relative 'merch_calendar/retail_calendar'
require_relative 'merch_calendar/fiscal_year_calendar'
28 changes: 0 additions & 28 deletions lib/merch_calendar/configurable.rb

This file was deleted.

13 changes: 0 additions & 13 deletions lib/merch_calendar/configuration.rb

This file was deleted.

118 changes: 118 additions & 0 deletions lib/merch_calendar/fiscal_year_calendar.rb
@@ -0,0 +1,118 @@
require "merch_calendar/retail_calendar"

module MerchCalendar
class FiscalYearCalendar
# Stitch Fix's fiscal year starts two quarters *before* (hence the negative number) the start of the
# merch/retail calendar year.
STITCH_FIX_FY_QUARTER_OFFSET = -2

QUARTER_1 = 1
QUARTER_4 = 4

# @param fy_quarter_offset [Fixnum]
# The number of quarters before or after the start of the traditional NRF retail calendar that the year
# should begin.
# ex) Stitch Fix's fiscal year calendar starts in August of the prior gregorian calendar year.
# February 2017 = Traditional retail month 1, year 2017
# August 2016 = Offset retail month 1, year 2017 (2 quarters earlier)
def initialize(fy_quarter_offset = STITCH_FIX_FY_QUARTER_OFFSET)
@fy_quarter_offset = fy_quarter_offset

# TODO: support other fiscal year offsets
if fy_quarter_offset != STITCH_FIX_FY_QUARTER_OFFSET
raise NotImplementedError.new("FY quarter offset of #{fy_quarter_offset} not yet supported")
end

@retail_calendar = RetailCalendar.new
end

# The date of the first day of the year
def start_of_year(year)
start_of_quarter(year, QUARTER_1)
end

# The date of the last day of the year
def end_of_year(year)
end_of_quarter(year, QUARTER_4)
end

# Return the starting date for a particular quarter
def start_of_quarter(year, quarter)
@retail_calendar.start_of_quarter(*offset_quarter(year, quarter))
end

# Return the ending date for a particular quarter
def end_of_quarter(year, quarter)
@retail_calendar.end_of_quarter(*offset_quarter(year, quarter))
end

# The date of the first day of the merch month
# @param [Fixnum] year - the fiscal year
# @param [Fixnum] merch_month - the nth month of the offset calendar
# ex) for an offset of +/- 2 quarters, month 1 = August
def start_of_month(year, merch_month)
@retail_calendar.start_of_month(*offset_month(year, merch_month))
end

# The date of the last day of the merch month
# @param [Fixnum] year - the fiscal year
# @param [Fixnum] merch_month - the nth month of the offset calendar
# ex) for an offset of +/- 2 quarters, month 1 = August
def end_of_month(year, merch_month)
@retail_calendar.end_of_month(*offset_month(year, merch_month))
end

# Returns the date that corresponds to the first day in the merch week
def start_of_week(year, merch_month, merch_week)
@retail_calendar.start_of_week(*offset_month(year, merch_month), merch_week)
end

# Returns the date that corresponds to the last day in the merch week
def end_of_week(year, merch_month, merch_week)
@retail_calendar.end_of_week(*offset_month(year, merch_month), merch_week)
end

# Returns the number of weeks in the fiscal year
def weeks_in_year(year)
@retail_calendar.weeks_in_year(offset_year(year))
end

private

# Offsets the quarter based on the fiscal year quarter offset
# returns: ofset [year, quarter]
def offset_quarter(year, quarter)
# first quarter in fiscal calendar is Q3 of retail calendar of previous year
if quarter >= 1 + @fy_quarter_offset.abs
[year, quarter + @fy_quarter_offset]
else
[year - 1, quarter - @fy_quarter_offset]
end
end

# Offsets the month based on the fiscal year quarter offset
# returns: ofset [year, month]
def offset_month(year, month)
# 3 - number of months in a quarter
month_offset = @fy_quarter_offset * 3

if month >= (month_offset.abs + 1)
[year, month + month_offset]
else
[year - 1, month - month_offset]
end
end

# Offsets the year based on the fiscal year quarter offset
# returns: ofset year
def offset_year(year)
if @fy_quarter_offset < 0
year -= 1
elsif @fy_quarter_offset > 0
year += 1
else
year
end
end
end
end

0 comments on commit f5059ab

Please sign in to comment.