diff --git a/.travis.yml b/.travis.yml index bcd8b90..4d994e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ -services: mongodb +services: + - mongodb + - mysql + - postgresql before_script: - "mysql -e 'create database by_star_test;'" diff --git a/README.md b/README.md index f3e8cb1..24a7d41 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ See sections below for detailed argument usage of each: | `by_month` | Query by month. Allows integer arg, e.g. `11` for November. | | `by_calendar_month` | Month as it appears on a calendar; days form previous/following months which are part of the first/last weeks of the given month. | | `by_quarter` | 3-month intervals of the year. | +| `by_semester` | 6-month intervals of the year. | | `by_year` | Query by year. Allows integer arg, e.g. `2017`. | ### Relative Scopes @@ -544,11 +545,41 @@ Post.by_quarter(2, year: 2012) This will return all posts in the 2nd quarter of 2012. ```ruby -Post.by_week(Time.local(2012,1,1)) +Post.by_quarter(Time.local(2012,1,1)) ``` This will return all posts from the first quarter of 2012. +### by_semester + +Finds records by 6-month biannual period of year. Semester numbering starts at 1. The two semesters of the year begin on Jan 1 and Jul 1 respectively. + +To find records from the current semester: + +```ruby +Post.by_semester +``` + +To find records based on a semester, you can pass in a number (representing the semester number) or a time object: + +```ruby +Post.by_semester(1) +``` + +This will return all posts in the 1st semester of the current year. + +```ruby +Post.by_semester(2, year: 2012) +``` + +This will return all posts in the 2nd semester of 2012. + +```ruby +Post.by_semester(Time.local(2012,1,1)) +``` + +This will return all posts from the first semester of 2012. + ## Version Support ByStar is tested against the following versions: diff --git a/lib/by_star/between.rb b/lib/by_star/between.rb index 8662151..049df02 100644 --- a/lib/by_star/between.rb +++ b/lib/by_star/between.rb @@ -123,6 +123,20 @@ def by_quarter(*args) end end + def by_semester(*args) + with_by_star_options(*args) do |time, options| + date = ByStar::Normalization.semester(time, options) + + first_semester_month = date.month - (5 + date.month) % 6 + beginning_of_semester = date.beginning_of_month.change(month: first_semester_month) + + last_semester_month = date.month + (12 - date.month) % 6 + end_of_semester = date.beginning_of_month.change(month: last_semester_month).end_of_month + + between_dates(beginning_of_semester, end_of_semester, options) + end + end + def by_year(*args) with_by_star_options(*args) do |time, options| date = ByStar::Normalization.year(time, options) diff --git a/lib/by_star/normalization.rb b/lib/by_star/normalization.rb index 1632b9b..2f4ee3d 100644 --- a/lib/by_star/normalization.rb +++ b/lib/by_star/normalization.rb @@ -68,6 +68,20 @@ def quarter_integer(value, options={}) time.beginning_of_year + ((value - 1) * 3).months end + def semester(value, options={}) + value = try_string_to_int(value) + case value + when Integer then semester_integer(value, options) + else date(value) + end + end + + def semester_integer(value, options={}) + raise ParseError, 'Semester number must be between 1 and 2' unless value.in?(1..2) + time = Time.zone.local(options[:year] || Time.zone.now.year) + time.beginning_of_year + ((value - 1) * 6).months + end + def month(value, options={}) value = try_string_to_int(value) case value diff --git a/spec/integration/active_record/active_record_spec.rb b/spec/integration/active_record/active_record_spec.rb index 907ac8c..ccdd9a7 100644 --- a/spec/integration/active_record/active_record_spec.rb +++ b/spec/integration/active_record/active_record_spec.rb @@ -32,6 +32,7 @@ it_behaves_like 'by month' it_behaves_like 'by calendar month' it_behaves_like 'by quarter' + it_behaves_like 'by semester' it_behaves_like 'by week' it_behaves_like 'by cweek' it_behaves_like 'by weekend' diff --git a/spec/integration/mongoid/mongoid_spec.rb b/spec/integration/mongoid/mongoid_spec.rb index 9b34fbe..e7e1a02 100644 --- a/spec/integration/mongoid/mongoid_spec.rb +++ b/spec/integration/mongoid/mongoid_spec.rb @@ -31,6 +31,7 @@ it_behaves_like 'by month' it_behaves_like 'by calendar month' it_behaves_like 'by quarter' + it_behaves_like 'by semester' it_behaves_like 'by week' it_behaves_like 'by cweek' it_behaves_like 'by weekend' diff --git a/spec/integration/shared/by_semester.rb b/spec/integration/shared/by_semester.rb new file mode 100644 index 0000000..26587e0 --- /dev/null +++ b/spec/integration/shared/by_semester.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +shared_examples_for 'by semester' do + + describe '#by_semester' do + + context 'point-in-time' do + subject { Post.by_semester(1) } + it { expect(subject.count).to eql(12) } + end + + context 'timespan' do + subject { Event.by_semester(1) } + it { expect(subject.count).to eql(14) } + end + + context 'timespan strict' do + subject { Event.by_semester(Date.parse('2014-02-01'), strict: true) } + it { expect(subject.count).to eql(9) } + end + + context 'with :year option' do + + context 'point-in-time' do + subject { Post.by_semester(2, year: 2013) } + it { expect(subject.count).to eql(10) } + end + + context 'timespan' do + subject { Event.by_semester(2, year: 2013) } + it { expect(subject.count).to eql(13) } + end + + context 'timespan strict' do + subject { Event.by_semester(2, year: 2013, strict: true) } + it { expect(subject.count).to eql(8) } + end + end + + it 'should raise an error when given an invalid argument' do + expect{ Post.by_semester(0) }.to raise_error(ByStar::ParseError, 'Semester number must be between 1 and 2') + expect{ Post.by_semester(5) }.to raise_error(ByStar::ParseError, 'Semester number must be between 1 and 2') + end + + it 'should be able to use an alternative field' do + expect(Event.by_semester(1, field: 'end_time').count).to eq(14) + end + end +end diff --git a/spec/unit/normalization_spec.rb b/spec/unit/normalization_spec.rb index 9a6ee3b..70072d2 100644 --- a/spec/unit/normalization_spec.rb +++ b/spec/unit/normalization_spec.rb @@ -317,6 +317,36 @@ specify { expect{ ByStar::Normalization.quarter(5) }.to raise_error(ByStar::ParseError, 'Quarter number must be between 1 and 4') } end end + + describe '#semester' do + subject { ByStar::Normalization.semester(input, options) } + it_behaves_like 'date normalization from string' + it_behaves_like 'date normalization from time value' + + context 'Integer 1' do + let(:input){ 1 } + it { expect eq Date.parse('2014-01-01') } + end + + context 'Integer 2' do + let(:input){ 2 } + it { expect eq Date.parse('2014-07-01') } + end + + context 'with year option' do + let(:options){ { year: 2011 } } + + context 'Integer 2' do + let(:input){ 2 } + it { expect eq Date.parse('2011-07-01') } + end + end + + context 'out of range' do + specify { expect{ ByStar::Normalization.semester(0) }.to raise_error(ByStar::ParseError, 'Semester number must be between 1 and 2') } + specify { expect{ ByStar::Normalization.semester(5) }.to raise_error(ByStar::ParseError, 'Semester number must be between 1 and 2') } + end + end describe '#year' do subject { ByStar::Normalization.year(input, options) }