Skip to content

Commit

Permalink
Merge branch 'fix-conversion'
Browse files Browse the repository at this point in the history
  • Loading branch information
hzamani committed Jun 23, 2024
2 parents 25a517d + 428c79a commit 66a54b4
Show file tree
Hide file tree
Showing 17 changed files with 287 additions and 190 deletions.
13 changes: 6 additions & 7 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: Ruby

on:
push:
branches: [master]
pull_request:
branches: [master]
on: [push, pull_request]

permissions:
contents: read
Expand All @@ -14,10 +10,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ["2.6", "2.7", "3.0", "3.1"]
ruby-version:
- '3.1'
- '3.2'
- '3.3'

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
Expand Down
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Parsi Date

The Parsi Date library is an implementation of the [Solar Hijri Calendar](http://en.wikipedia.org/wiki/Solar_Hijri_calendar), also known as the Jalali or Persian Calendar, which is the official calendar of Iran. This library aims to create a Solar Hijri date library that closely resembles Ruby's built-in date library, providing a seamless experience for users who need to work with the Solar Hijri dates. The conversion algorithm used in this library is adopted from [FarsiWeb](http://www.farsiweb.info/jalali/jalali.c).

## Features

- **Parsi::Date** and **Parsi::DateTime**: These objects can be used just like Ruby's `Date` and `DateTime` objects.
- **Date Conversion:** Easily convert between Gregorian and Solar Hijri dates.
- **Leap Year Calculation:** Determine if a given Solar Hijri year is a leap year.
- **Date Parsing and Formatting:** Parse Solar Hijri date strings and format dates in various styles.

## Usage

### Basic Operations

You can use `Parsi::Date` and `Parsi::DateTime` objects in the same way you use `Date` and `DateTime` objects in Ruby. Here are some examples:

```ruby
# Get today's Solar Hijri date
a = Parsi::Date.today
# => #<Parsi::Date: 1391-08-09 (4912461/2,0/1)>

# Add 12 months to it
b = a >> 12
# => #<Parsi::Date: 1392-08-09 (4913193/2,0/1)>

# Count the number of Sundays between two dates
a.upto(b).select{ |d| d.sunday? }.count
# => 52

# Check if a given year is a leap year
Parsi::Date.leap?(1391)
# => true
```

### Parsing and Formatting Dates

You can parse Solar Hijri date strings and format them in various styles:

```ruby
# Parse a Solar Hijri date string
c = Parsi::Date.parse("1391/10/12")
# => #<Parsi::Date: 1391-10-12 (4912587/2,0)>

# Format the date in Persian
c.strftime("%A %d %B %Y")
# => "سه‌شنبه 12 دی 1391"

# Format the date in English
c.strftime("%^EA %d %^EB %Y")
# => "Seshambe 12 Day 1391"
```

### Converting Between Calendars

You can easily convert between Gregorian and Solar Hijri dates:

```ruby
# Convert a Gregorian date to Solar Hijri
d = Date.civil(2012, 10, 30).to_parsi
# => #<Parsi::Date: 1391-08-09 (4912461/2,0/1)>

# Convert a Solar Hijri date back to Gregorian
d.to_gregorian
# => #<Date: 2012-10-30 ((2456231j,0s,0n),+0s,2299161j)>
```

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'parsi_date'
```

And then execute:

```sh
bundle install
```

Or install it yourself as:

```sh
gem install parsi_date
```

## License

The Parsi Date library is open-source software licensed under the MIT license.

## Contributing

If you would like to contribute to the development of the Parsi Date library, please feel free to fork the repository and submit pull requests. Contributions, bug reports, and feature requests are welcome and appreciated.
31 changes: 0 additions & 31 deletions README.rdoc

This file was deleted.

60 changes: 32 additions & 28 deletions lib/parsi-date.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,11 @@ class Date
JALALI_EPOCH_IN_AJD = Rational(3896641, 2) # :nodoc:
MJD_EPOCH_IN_AJD = Rational(4800001, 2) # 1858-11-17 # :nodoc:
UNIX_EPOCH_IN_AJD = Rational(4881175, 2) # 1970-01-01 # :nodoc:
JALALI_EPOCH_IN_CJD = 1948321 # :nodoc:
JALALI_EPOCH_IN_CJD = 1948320 # :nodoc:
MJD_EPOCH_IN_CJD = 2400001 # :nodoc:
UNIX_EPOCH_IN_CJD = 2440588 # :nodoc:
LD_EPOCH_IN_CJD = 2299160 # :nodoc:
DAYS_IN_MONTH = [nil, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] # :nodoc:
DAYS_IN_MONTH = [0, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] # :nodoc:

LEAP_REMINDERS = [1, 5, 9, 13, 17, 22, 26, 30].freeze

Expand All @@ -153,38 +153,43 @@ def leap? year

private

DAYS_TO_FIRST_OF_MONTH = [nil, 0, 31, 62, 93, 124, 155, 186, 216, 246, 276, 306, 336] # :nodoc:

# Convert a Civil Date to a Julian Day Number and returns the corresponding Julian Day Number.
def civil_to_jd year, month, day # :nodoc:
epbase = year - 474
epyear = 474 + (epbase % 2820)

day + DAYS_TO_FIRST_OF_MONTH[month] +
(epyear * 682 - 110) / 2816 +
(epyear - 1) * 365 +
(epbase / 2820 * 1029983) +
(JALALI_EPOCH_IN_CJD - 1)
def civil_to_jd(year, month, day) # :nodoc:
year -= 1
month -= 1
day -= 1

jd = 365*year + (year/33)*8 + (year%33+3)/4;
while month > 0
jd += DAYS_IN_MONTH[month]
month -= 1
end

jd + day + JALALI_EPOCH_IN_CJD
end

# Convert a Julian Day Number to a Civil Date. +jday+ is the Julian Day Number.
#
# Returns the corresponding [year, month, day_of_month] as a three-element array.
def jd_to_civil jday
depoch = (jday - first_day_of_year(475))
cycle, cyear = depoch.divmod 1029983
def jd_to_civil jd
jd -= JALALI_EPOCH_IN_CJD

if cyear == 1029982
ycycle = 2820
else
aux1, aux2 = cyear.divmod 366
ycycle = (2134 * aux1 + 2816 * aux2 + 2815) / 1028522 + aux1 + 1
n, jd = jd.divmod 12053
m, jd = jd.divmod 1461
year = 33*n + 4*m + 1

if jd >= 366
n, jd = (jd - 1).divmod 365
year += n
end

month = 1
while month < 12 && jd >= DAYS_IN_MONTH[month]
jd -= DAYS_IN_MONTH[month]
month += 1
end
year = ycycle + 2820 * cycle + 474
yday = jday - first_day_of_year(year) + 1
month = ((yday <= 186) ? yday / 31.0 : (yday - 6) / 30.0).ceil
day = (jday - first_day_of_month(year, month) + 1)
[year, month, day]

[year, month, jd + 1]
end

# Do +year+, +month+, and day-of-month +day+ make a valid Civil Date?
Expand Down Expand Up @@ -332,7 +337,6 @@ def valid_civil? year, month, day
# Parsi::Date.jd # => #<Parsi::Date: -5335-09-01>
#
def jd jday=0
jd = _valid_jd? jday
new! jd_to_ajd(jday, 0, 0), 0
end

Expand Down Expand Up @@ -733,7 +737,7 @@ def to_s() format('%.4d-%02d-%02d', year, mon, mday) end
def marshal_dump() [@ajd, @offset] end

# Load from Marshal format.
def marshal_load(a) @ajd, @of, = a end
def marshal_load(a) @ajd, @offset, = a end
end
end

Expand Down
1 change: 1 addition & 0 deletions parsi-date.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Gem::Specification.new do |s|
s.require_paths = ['lib']

s.add_development_dependency("bundler", "~> 2.0")
s.add_development_dependency("json", "~> 2.0")
s.add_development_dependency("rake", "~> 13.0")
s.add_development_dependency("rspec", "~> 3.0")
s.add_development_dependency("activerecord", "~> 6.0")
Expand Down
37 changes: 23 additions & 14 deletions spec/parsi-date/accessor_spec.rb
Original file line number Diff line number Diff line change
@@ -1,57 +1,66 @@
describe "Parsi::Date#ajd" do
it "determines the Astronomical Julian day" do
expect Parsi::Date.civil(1391, 8, 6).ajd == Rational(4912455, 2)
expect(Parsi::Date.civil(1391, 8, 6).ajd).to be == Rational(4912455, 2)
expect(Parsi::Date.civil(1403, 12, 30).ajd).to be == Rational(4921509, 2)
end
end

describe "Parsi::Date#amjd" do
it "determines the Astronomical Modified Julian day" do
expect Parsi::Date.civil(1391, 8, 6).amjd == 56227
expect(Parsi::Date.civil(1391, 8, 6).amjd).to be == 56227
expect(Parsi::Date.civil(1403, 12, 30).amjd).to be == 60754
end
end

describe "Parsi::Date#mjd" do
it "determines the Modified Julian day" do
expect Parsi::Date.civil(1391, 8, 6).mjd == 56227
expect(Parsi::Date.civil(1391, 8, 6).mjd).to be == 56227
expect(Parsi::Date.civil(1403, 12, 30).mjd).to be == 60754
end
end

describe "Parsi::Date#ld" do
it "determines the Modified Julian day" do
expect Parsi::Date.civil(1391, 8, 6).ld == 157068
it "determines the number of days since the Day of Calendar Reform" do
expect(Parsi::Date.civil(1391, 8, 6).ld).to be == 157068
expect(Parsi::Date.civil(1403, 12, 30).ld).to be == 161595
end
end

describe "Parsi::Date#year" do
it "determines the year" do
expect Parsi::Date.civil(1391, 8, 6).year == 1391
expect(Parsi::Date.civil(1391, 8, 6).year).to be == 1391
expect(Parsi::Date.civil(1403, 12, 30).year).to be == 1403
end
end

describe "Parsi::Date#yday" do
it "determines the year" do
expect Parsi::Date.civil(1391, 1, 17).yday == 17
expect Parsi::Date.civil(1391, 8, 6).yday == 222
expect(Parsi::Date.civil(1391, 1, 17).yday).to be == 17
expect(Parsi::Date.civil(1391, 8, 6).yday).to be == 222
expect(Parsi::Date.civil(1403, 12, 30).yday).to be == 366
end
end

describe "Parsi::Date#mon" do
it "determines the month" do
expect Parsi::Date.civil(1391, 1, 17).mon == 1
expect Parsi::Date.civil(1391, 8, 6).mon == 8
expect(Parsi::Date.civil(1391, 1, 17).mon).to be == 1
expect(Parsi::Date.civil(1391, 8, 6).mon).to be == 8
expect(Parsi::Date.civil(1403, 12, 30).mon).to be == 12
end
end

describe "Parsi::Date#mday" do
it "determines the day of the month" do
expect Parsi::Date.civil(1391, 1, 17).mday == 17
expect Parsi::Date.civil(1391, 10, 28).mday == 28
expect(Parsi::Date.civil(1391, 1, 17).mday).to be == 17
expect(Parsi::Date.civil(1391, 10, 28).mday).to be == 28
expect(Parsi::Date.civil(1403, 12, 30).mday).to be == 30
end
end

describe "Parsi::Date#wday" do
it "determines the week day" do
expect Parsi::Date.civil(1391, 1, 17).wday == 4
expect Parsi::Date.civil(1391, 8, 6).wday == 6
expect(Parsi::Date.civil(1391, 1, 17).wday).to be == 4
expect(Parsi::Date.civil(1391, 8, 6).wday).to be == 6
expect(Parsi::Date.civil(1403, 12, 30).wday).to be == 4
end
end
1 change: 1 addition & 0 deletions spec/parsi-date/accessors_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'active_record'
require 'sqlite3'
require 'json'

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")

Expand Down
6 changes: 4 additions & 2 deletions spec/parsi-date/add_month_spec.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
describe "Parsi::Date#>>" do
it "adds the number of months to a Parsi::Date" do
expect (Parsi::Date.civil(1391, 2, 27) >> 10) == Parsi::Date.civil(1391, 12, 27)
expect(Parsi::Date.civil(1391, 2, 27) >> 10).to be == Parsi::Date.civil(1391, 12, 27)
expect(Parsi::Date.civil(1403, 11, 30) >> 1).to be == Parsi::Date.civil(1403, 12, 30)
end

it "sets the day to the last day of a month if the day doesn't exist" do
expect (Parsi::Date.civil(1391, 6, 31) >> 1) == Parsi::Date.civil(1391, 7, 30)
expect(Parsi::Date.civil(1391, 6, 31) >> 1).to be == Parsi::Date.civil(1391, 7, 30)
expect(Parsi::Date.civil(1402, 11, 30) >> 1).to be == Parsi::Date.civil(1402, 12, 29)
end

it "raise a TypeError when passed a Symbol" do
Expand Down
6 changes: 4 additions & 2 deletions spec/parsi-date/add_spec.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
describe "Parsi::Date#+" do
it "adds the number of days to a Parsi::Date" do
expect Parsi::Date.civil(1391, 2, 27) + 10 == Parsi::Date.civil(1391, 3, 6)
expect(Parsi::Date.civil(1391, 2, 27) + 10).to be == Parsi::Date.civil(1391, 3, 6)
expect(Parsi::Date.civil(1403, 12, 29) + 1).to be == Parsi::Date.civil(1403, 12, 30)
expect(Parsi::Date.civil(1403, 12, 30) + 1).to be == Parsi::Date.civil(1404, 1, 1)
end

it "adds a negative number of days to a Parsi::Date" do
expect Parsi::Date.civil(1391, 2, 27) + (-10) == Parsi::Date.civil(1391, 2, 17)
expect(Parsi::Date.civil(1391, 2, 27) + (-10)).to be == Parsi::Date.civil(1391, 2, 17)
end

it "raises a TypeError when passed a Symbol" do
Expand Down
Loading

0 comments on commit 66a54b4

Please sign in to comment.