Skip to content

Commit cf89614

Browse files
authoredMar 12, 2018
Merge pull request #5076 from jruby/date_native2
redo date.rb (most) parts in native
2 parents b75044c + 2d16dd2 commit cf89614

28 files changed

+3685
-1739
lines changed
 

Diff for: ‎core/src/main/java/org/jruby/Ruby.java

+2
Original file line numberDiff line numberDiff line change
@@ -5251,6 +5251,8 @@ protected TypePopulator computeValue(Class<?> type) {
52515251
// I know of use very few of them. Even if there are many the size of these lists are modest.
52525252
private final Map<String, List<StrptimeToken>> strptimeFormatCache = new ConcurrentHashMap<>();
52535253

5254+
transient RubyString tzVar;
5255+
52545256
@Deprecated
52555257
private void setNetworkStack() {
52565258
deprecatedNetworkStackProperty();

Diff for: ‎core/src/main/java/org/jruby/RubyHash.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,8 @@ private final void alloc() {
345345
{ head.prevAdded = head.nextAdded = head; }
346346

347347
public static final class RubyHashEntry implements Map.Entry {
348-
private IRubyObject key;
349-
private IRubyObject value;
348+
IRubyObject key;
349+
IRubyObject value;
350350
private RubyHashEntry next;
351351
private RubyHashEntry prevAdded;
352352
private RubyHashEntry nextAdded;
@@ -580,6 +580,10 @@ protected RubyHashEntry internalGetEntry(IRubyObject key) {
580580
return NO_ENTRY;
581581
}
582582

583+
final RubyHashEntry getEntry(IRubyObject key) {
584+
return internalGetEntry(key);
585+
}
586+
583587
private boolean internalKeyExist(RubyHashEntry entry, int hash, IRubyObject key) {
584588
return (entry.hash == hash
585589
&& (entry.key == key || (!isComparedByIdentity() && key.eql(entry.key))));

Diff for: ‎core/src/main/java/org/jruby/RubyNumeric.java

+15-6
Original file line numberDiff line numberDiff line change
@@ -179,20 +179,29 @@ public static int num2int(IRubyObject arg) {
179179
/** check_int
180180
*
181181
*/
182+
public static int checkInt(final Ruby runtime, long num){
183+
if (num < Integer.MIN_VALUE) {
184+
tooSmall(runtime, num);
185+
} else if (num > Integer.MAX_VALUE) {
186+
tooBig(runtime, num);
187+
}
188+
return (int) num;
189+
}
190+
182191
public static void checkInt(IRubyObject arg, long num){
183192
if (num < Integer.MIN_VALUE) {
184-
tooSmall(arg, num);
193+
tooSmall(arg.getRuntime(), num);
185194
} else if (num > Integer.MAX_VALUE) {
186-
tooBig(arg, num);
195+
tooBig(arg.getRuntime(), num);
187196
}
188197
}
189198

190-
private static void tooSmall(IRubyObject arg, long num) {
191-
throw arg.getRuntime().newRangeError("integer " + num + " too small to convert to `int'");
199+
private static void tooSmall(Ruby runtime, long num) {
200+
throw runtime.newRangeError("integer " + num + " too small to convert to `int'");
192201
}
193202

194-
private static void tooBig(IRubyObject arg, long num) {
195-
throw arg.getRuntime().newRangeError("integer " + num + " too big to convert to `int'");
203+
private static void tooBig(Ruby runtime, long num) {
204+
throw runtime.newRangeError("integer " + num + " too big to convert to `int'");
196205
}
197206

198207
/**

Diff for: ‎core/src/main/java/org/jruby/RubyTime.java

+24-17
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
import org.jruby.runtime.Block;
5454
import org.jruby.runtime.ClassIndex;
5555
import org.jruby.runtime.Helpers;
56-
import org.jruby.runtime.JavaSites;
5756
import org.jruby.runtime.JavaSites.TimeSites;
5857
import org.jruby.runtime.ObjectAllocator;
5958
import org.jruby.runtime.ThreadContext;
@@ -66,11 +65,9 @@
6665

6766
import java.math.BigDecimal;
6867
import java.math.BigInteger;
69-
import java.math.RoundingMode;
7068
import java.util.Calendar;
7169
import java.util.Date;
7270
import java.util.GregorianCalendar;
73-
import java.util.HashMap;
7471
import java.util.Locale;
7572
import java.util.Map;
7673
import java.util.regex.Matcher;
@@ -136,23 +133,32 @@ public ClassIndex getNativeClassIndex() {
136133
return ClassIndex.TIME;
137134
}
138135

139-
private static IRubyObject getEnvTimeZone(Ruby runtime) {
140-
RubyString tzVar = (RubyString) runtime.getTime().getInternalVariable("tz_string");
141-
if (tzVar == null) {
142-
tzVar = runtime.newString(TZ_STRING);
143-
tzVar.setFrozen(true);
144-
runtime.getTime().setInternalVariable("tz_string", tzVar);
136+
private static transient Object[] tzValue; // (RubyString, String) - assuming single runtime or same ENV['TZ']
137+
138+
public static String getEnvTimeZone(Ruby runtime) {
139+
RubyString tz = runtime.tzVar;
140+
if (tz == null) {
141+
tz = runtime.newString(TZ_STRING);
142+
tz.setFrozen(true);
143+
runtime.tzVar = tz;
145144
}
146-
return runtime.getENV().op_aref(runtime.getCurrentContext(), tzVar);
145+
146+
RubyHash.RubyHashEntry entry = runtime.getENV().getEntry(tz);
147+
if (entry.key == null || entry.key == NEVER) return null; // NO_ENTRY
148+
149+
if (entry.key != tz) runtime.tzVar = (RubyString) entry.key;
150+
151+
Object[] tzVal = tzValue;
152+
if (tzVal != null && tzVal[0] == entry.value) return (String) tzVal[1]; // cache RubyString -> String
153+
154+
final String val = (entry.value instanceof RubyString) ? ((RubyString) entry.value).asJavaString() : null;
155+
tzValue = new Object[] { entry.value, val };
156+
return val;
147157
}
148158

149159
public static DateTimeZone getLocalTimeZone(Ruby runtime) {
150-
IRubyObject tz = getEnvTimeZone(runtime);
151-
152-
if (tz == null || ! (tz instanceof RubyString)) {
153-
return DateTimeZone.getDefault();
154-
}
155-
return getTimeZoneFromTZString(runtime, tz.toString());
160+
final String tz = getEnvTimeZone(runtime);
161+
return tz == null ? DateTimeZone.getDefault() : getTimeZoneFromTZString(runtime, tz);
156162
}
157163

158164
public static DateTimeZone getTimeZoneFromTZString(Ruby runtime, String zone) {
@@ -918,7 +924,8 @@ public IRubyObject zone() {
918924
}
919925

920926
public static String getRubyTimeZoneName(Ruby runtime, DateTime dt) {
921-
return RubyTime.getRubyTimeZoneName(getEnvTimeZone(runtime).toString(), dt);
927+
final String tz = getEnvTimeZone(runtime);
928+
return RubyTime.getRubyTimeZoneName(tz == null ? "" : tz, dt);
922929
}
923930

924931
public static String getRubyTimeZoneName(String envTZ, DateTime dt) {

Diff for: ‎core/src/main/java/org/jruby/ext/date/DateLibrary.java

+9-5
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66

77
import java.io.IOException;
88

9-
/**
10-
* Assumes kernel will lo
11-
*/
129
public class DateLibrary implements Library {
10+
11+
public static void load(Ruby runtime) {
12+
RubyClass Date = RubyDate.createDateClass(runtime);
13+
RubyDateTime.createDateTimeClass(runtime, Date);
14+
TimeExt.load(runtime);
15+
}
16+
1317
public void load(final Ruby runtime, boolean wrap) throws IOException {
14-
RubyClass dateClass = runtime.getClass("Date");
15-
dateClass.defineAnnotatedMethods(RubyDate.class);
18+
DateLibrary.load(runtime);
1619
}
20+
1721
}

Diff for: ‎core/src/main/java/org/jruby/ext/date/DateUtils.java

+449
Large diffs are not rendered by default.

Diff for: ‎core/src/main/java/org/jruby/ext/date/RubyDate.java

+1,592-20
Large diffs are not rendered by default.

Diff for: ‎core/src/main/java/org/jruby/ext/date/RubyDateTime.java

+473
Large diffs are not rendered by default.

Diff for: ‎core/src/main/java/org/jruby/ext/date/TimeExt.java

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
**** BEGIN LICENSE BLOCK *****
3+
* Version: EPL 1.0/GPL 2.0/LGPL 2.1
4+
*
5+
* The contents of this file are subject to the Eclipse Public
6+
* License Version 1.0 (the "License"); you may not use this file
7+
* except in compliance with the License. You may obtain a copy of
8+
* the License at http://www.eclipse.org/legal/epl-v10.html
9+
*
10+
* Software distributed under the License is distributed on an "AS
11+
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
12+
* implied. See the License for the specific language governing
13+
* rights and limitations under the License.
14+
*
15+
* Copyright (C) 2018 The JRuby Team
16+
*
17+
* Alternatively, the contents of this file may be used under the terms of
18+
* either of the GNU General Public License Version 2 or later (the "GPL"),
19+
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
20+
* in which case the provisions of the GPL or the LGPL are applicable instead
21+
* of those above. If you wish to allow use of your version of this file only
22+
* under the terms of either the GPL or the LGPL, and not to allow others to
23+
* use your version of this file under the terms of the EPL, indicate your
24+
* decision by deleting the provisions above and replace them with the notice
25+
* and other provisions required by the GPL or the LGPL. If you do not delete
26+
* the provisions above, a recipient may use your version of this file under
27+
* the terms of any one of the EPL, the GPL or the LGPL.
28+
***** END LICENSE BLOCK *****/
29+
package org.jruby.ext.date;
30+
31+
import org.joda.time.DateTime;
32+
import org.jruby.*;
33+
import org.jruby.anno.JRubyMethod;
34+
import org.jruby.runtime.ThreadContext;
35+
import org.jruby.runtime.builtin.IRubyObject;
36+
37+
import static org.jruby.ext.date.DateUtils.*;
38+
import static org.jruby.ext.date.RubyDate.*;
39+
40+
/**
41+
* Time's extensions from `require 'date'`
42+
*
43+
* @author kares
44+
*/
45+
public abstract class TimeExt {
46+
47+
private TimeExt() { /* no instances */ }
48+
49+
static void load(Ruby runtime) {
50+
runtime.getTime().defineAnnotatedMethods(TimeExt.class);
51+
}
52+
53+
@JRubyMethod
54+
public static RubyTime to_time(IRubyObject self) { return (RubyTime) self; }
55+
56+
@JRubyMethod(name = "to_date")
57+
public static RubyDate to_date(ThreadContext context, IRubyObject self) {
58+
final DateTime dt = ((RubyTime) self).getDateTime();
59+
long jd = civil_to_jd(dt.getYear(), dt.getMonthOfYear(), dt.getDayOfMonth(), GREGORIAN);
60+
return new RubyDate(context, getDate(context.runtime), jd_to_ajd(context, jd), CHRONO_ITALY_UTC, 0);
61+
}
62+
63+
@JRubyMethod(name = "to_datetime")
64+
public static RubyDateTime to_datetime(ThreadContext context, IRubyObject self) {
65+
final RubyTime time = (RubyTime) self;
66+
DateTime dt = ((RubyTime) self).getDateTime();
67+
68+
long subMillisNum = 0, subMillisDen = 1;
69+
if (time.getNSec() != 0) {
70+
IRubyObject subMillis = RubyRational.newRationalCanonicalize(context, time.getNSec(), 1_000_000);
71+
if (subMillis instanceof RubyRational) {
72+
subMillisNum = ((RubyRational) subMillis).getNumerator().getLongValue();
73+
subMillisDen = ((RubyRational) subMillis).getDenominator().getLongValue();
74+
}
75+
else {
76+
subMillisNum = ((RubyInteger) subMillis).getLongValue();
77+
}
78+
}
79+
80+
final int off = dt.getZone().getOffset(dt.getMillis()) / 1000;
81+
82+
int year = dt.getYear(); if (year <= 0) year--; // JODA's Julian chronology (no year 0)
83+
84+
if (year == 1582) { // take the "slow" path - JODA isn't adjusting for missing (reform) dates
85+
return calcAjdFromCivil(context, dt, off, subMillisNum, subMillisDen);
86+
}
87+
88+
dt = new DateTime(
89+
year, dt.getMonthOfYear(), dt.getDayOfMonth(),
90+
dt.getHourOfDay(), dt.getMinuteOfHour(), dt.getSecondOfMinute(),
91+
dt.getMillisOfSecond(), getChronology(context, ITALY, dt.getZone())
92+
);
93+
94+
return new RubyDateTime(context.runtime, getDateTime(context.runtime), dt, off, ITALY, subMillisNum, subMillisDen);
95+
}
96+
97+
private static RubyDateTime calcAjdFromCivil(ThreadContext context, final DateTime dt, final int off,
98+
final long subMillisNum, final long subMillisDen) {
99+
final Ruby runtime = context.runtime;
100+
101+
long jd = civil_to_jd(dt.getYear(), dt.getMonthOfYear(), dt.getDayOfMonth(), ITALY);
102+
RubyNumeric fr = timeToDayFraction(context, dt.getHourOfDay(), dt.getMinuteOfHour(), dt.getSecondOfMinute());
103+
104+
final RubyNumeric ajd = jd_to_ajd(context, jd, fr, off);
105+
RubyDateTime dateTime = new RubyDateTime(context, getDateTime(runtime), ajd, off, ITALY);
106+
dateTime.dt = dateTime.dt.withMillisOfSecond(dt.getMillisOfSecond());
107+
dateTime.subMillisNum = subMillisNum; dateTime.subMillisDen = subMillisDen;
108+
return dateTime;
109+
}
110+
111+
}

Diff for: ‎core/src/main/java/org/jruby/util/RubyDateFormatter.java

+49-37
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,28 @@
5151
import org.joda.time.chrono.GJChronology;
5252
import org.joda.time.chrono.JulianChronology;
5353
import org.jruby.Ruby;
54+
import org.jruby.RubyNumeric;
5455
import org.jruby.RubyString;
5556
import org.jruby.RubyTime;
5657
import org.jruby.lexer.StrftimeLexer;
5758
import org.jruby.runtime.ThreadContext;
58-
import org.jruby.runtime.builtin.IRubyObject;
5959

6060
import static org.jruby.util.RubyDateFormatter.FieldType.*;
6161

6262
public class RubyDateFormatter {
63-
private static final DateFormatSymbols FORMAT_SYMBOLS = new DateFormatSymbols(Locale.US);
63+
64+
private static final String[] FORMAT_MONTHS;
65+
private static final String[] FORMAT_SHORT_MONTHS;
66+
private static final String[] FORMAT_WEEKDAYS;
67+
private static final String[] FORMAT_SHORT_WEEKDAYS;
68+
static {
69+
final DateFormatSymbols FORMAT_SYMBOLS = new DateFormatSymbols(Locale.US);
70+
FORMAT_MONTHS = FORMAT_SYMBOLS.getMonths();
71+
FORMAT_SHORT_MONTHS = FORMAT_SYMBOLS.getShortMonths();
72+
FORMAT_WEEKDAYS = FORMAT_SYMBOLS.getWeekdays();
73+
FORMAT_SHORT_WEEKDAYS = FORMAT_SYMBOLS.getShortWeekdays();
74+
}
75+
6476
private static final Token[] CONVERSION2TOKEN = new Token[256];
6577

6678
private final Ruby runtime;
@@ -247,12 +259,12 @@ public List<Token> compilePattern(RubyString format, boolean dateLibrary) {
247259
}
248260

249261
public List<Token> compilePattern(ByteList pattern, boolean dateLibrary) {
250-
List<Token> compiledPattern = new LinkedList<Token>();
251-
252262
Encoding enc = pattern.getEncoding();
253263
if (!enc.isAsciiCompatible()) {
254264
throw runtime.newArgumentError("format should have ASCII compatible encoding");
255265
}
266+
267+
final List<Token> compiledPattern = new LinkedList<Token>();
256268
if (enc != ASCIIEncoding.INSTANCE) { // default for ByteList
257269
compiledPattern.add(new Token(Format.FORMAT_ENCODING, enc));
258270
}
@@ -356,21 +368,20 @@ enum FieldType {
356368
}
357369

358370
/** Convenience method when using no pattern caching */
359-
public RubyString compileAndFormat(RubyString pattern, boolean dateLibrary, DateTime dt, long nsec, IRubyObject sub_millis) {
371+
public RubyString compileAndFormat(RubyString pattern, boolean dateLibrary, DateTime dt, long nsec, RubyNumeric sub_millis) {
360372
RubyString out = format(compilePattern(pattern, dateLibrary), dt, nsec, sub_millis);
361-
if (pattern.isTaint()) {
362-
out.setTaint(true);
363-
}
373+
out.setEncoding(pattern.getEncoding());
374+
if (pattern.isTaint()) out.setTaint(true);
364375
return out;
365376
}
366377

367-
public RubyString format(List<Token> compiledPattern, DateTime dt, long nsec, IRubyObject sub_millis) {
378+
public RubyString format(List<Token> compiledPattern, DateTime dt, long nsec, RubyNumeric sub_millis) {
368379
return runtime.newString(formatToByteList(compiledPattern, dt, nsec, sub_millis));
369380
}
370381

371-
public ByteList formatToByteList(List<Token> compiledPattern, DateTime dt, long nsec, IRubyObject sub_millis) {
382+
private ByteList formatToByteList(List<Token> compiledPattern, DateTime dt, long nsec, RubyNumeric sub_millis) {
372383
RubyTimeOutputFormatter formatter = RubyTimeOutputFormatter.DEFAULT_FORMATTER;
373-
ByteList toAppendTo = new ByteList();
384+
final ByteList toAppendTo = new ByteList(24);
374385

375386
for (Token token: compiledPattern) {
376387
CharSequence output = null;
@@ -390,25 +401,19 @@ public ByteList formatToByteList(List<Token> compiledPattern, DateTime dt, long
390401
break;
391402
case FORMAT_WEEK_LONG:
392403
// This is GROSS, but Java API's aren't ISO 8601 compliant at all
393-
int v = (dt.getDayOfWeek()+1)%8;
394-
if(v == 0) {
395-
v++;
396-
}
397-
output = FORMAT_SYMBOLS.getWeekdays()[v];
404+
int v = (dt.getDayOfWeek() + 1) % 8;
405+
output = FORMAT_WEEKDAYS[v == 0 ? 1 : v];
398406
break;
399407
case FORMAT_WEEK_SHORT:
400408
// This is GROSS, but Java API's aren't ISO 8601 compliant at all
401-
v = (dt.getDayOfWeek()+1)%8;
402-
if(v == 0) {
403-
v++;
404-
}
405-
output = FORMAT_SYMBOLS.getShortWeekdays()[v];
409+
v = (dt.getDayOfWeek() + 1) % 8;
410+
output = FORMAT_SHORT_WEEKDAYS[v == 0 ? 1 : v];
406411
break;
407412
case FORMAT_MONTH_LONG:
408-
output = FORMAT_SYMBOLS.getMonths()[dt.getMonthOfYear()-1];
413+
output = FORMAT_MONTHS[dt.getMonthOfYear() - 1];
409414
break;
410415
case FORMAT_MONTH_SHORT:
411-
output = FORMAT_SYMBOLS.getShortMonths()[dt.getMonthOfYear()-1];
416+
output = FORMAT_SHORT_MONTHS[dt.getMonthOfYear() - 1];
412417
break;
413418
case FORMAT_DAY:
414419
type = NUMERIC2;
@@ -512,16 +517,10 @@ public ByteList formatToByteList(List<Token> compiledPattern, DateTime dt, long
512517
output = RubyTimeOutputFormatter.formatNumber(dt.getMillisOfSecond(), 3, '0');
513518
if (width > 3) {
514519
StringBuilder buff = new StringBuilder(output.length() + 6).append(output);
515-
if (sub_millis == null || sub_millis.isNil()) { // Time
520+
if (sub_millis == null) { // Time
516521
buff.append(RubyTimeOutputFormatter.formatNumber(nsec, 6, '0'));
517522
} else { // Date, DateTime
518-
int prec = width - 3;
519-
final ThreadContext context = runtime.getCurrentContext(); // TODO really need the dynamic nature here?
520-
IRubyObject power = runtime.newFixnum(10).op_pow(context, prec);
521-
IRubyObject truncated = sub_millis.callMethod(context, "numerator").callMethod(context, "*", power);
522-
truncated = truncated.callMethod(context, "/", sub_millis.callMethod(context, "denominator"));
523-
long decimals = truncated.convertToInteger().getLongValue();
524-
buff.append(RubyTimeOutputFormatter.formatNumber(decimals, prec, '0'));
523+
formatSubMillisGt3(runtime, buff, width, sub_millis);
525524
}
526525
output = buff;
527526
}
@@ -569,6 +568,18 @@ public ByteList formatToByteList(List<Token> compiledPattern, DateTime dt, long
569568
return toAppendTo;
570569
}
571570

571+
private static void formatSubMillisGt3(final Ruby runtime, final StringBuilder buff,
572+
final int width, RubyNumeric sub_millis) {
573+
final int prec = width - 3;
574+
final ThreadContext context = runtime.getCurrentContext();
575+
RubyNumeric power = (RubyNumeric) runtime.newFixnum(10).op_pow(context, prec);
576+
RubyNumeric truncated = (RubyNumeric) sub_millis.numerator(context).
577+
convertToInteger().op_mul(context, power);
578+
truncated = (RubyNumeric) truncated.idiv(context, sub_millis.denominator(context));
579+
long decimals = truncated.convertToInteger().getLongValue();
580+
buff.append(RubyTimeOutputFormatter.formatNumber(decimals, prec, '0'));
581+
}
582+
572583
/**
573584
* Ruby always follows Astronomical year numbering,
574585
* that is BC x is -x+1 and there is a year 0 (BC 1)
@@ -587,8 +598,7 @@ private static int formatWeekYear(DateTime dt, int firstDayOfWeek) {
587598
dtCalendar.setFirstDayOfWeek(firstDayOfWeek);
588599
dtCalendar.setMinimalDaysInFirstWeek(7);
589600
int value = dtCalendar.get(java.util.Calendar.WEEK_OF_YEAR);
590-
if ((value == 52 || value == 53) &&
591-
(dtCalendar.get(Calendar.MONTH) == Calendar.JANUARY )) {
601+
if ((value == 52 || value == 53) && (dtCalendar.get(Calendar.MONTH) == Calendar.JANUARY )) {
592602
// MRI behavior: Week values are monotonous.
593603
// So, weeks that effectively belong to previous year,
594604
// will get the value of 0, not 52 or 53, as in Java.
@@ -608,8 +618,8 @@ private static StringBuilder formatZone(int colons, int value, RubyTimeOutputFor
608618
hours = -hours;
609619
}
610620

611-
String mm = RubyTimeOutputFormatter.formatNumber(minutes, 2, '0');
612-
String ss = RubyTimeOutputFormatter.formatNumber(seconds, 2, '0');
621+
CharSequence mm = RubyTimeOutputFormatter.formatNumber(minutes, 2, '0');
622+
CharSequence ss = RubyTimeOutputFormatter.formatNumber(seconds, 2, '0');
613623

614624
char padder = formatter.getPadder('0');
615625
int defaultWidth = -1;
@@ -643,10 +653,12 @@ private static StringBuilder formatZone(int colons, int value, RubyTimeOutputFor
643653
width = minWidth;
644654
}
645655
width -= after.length();
646-
CharSequence before = RubyTimeOutputFormatter.formatSignedNumber(hours, width, padder);
656+
StringBuilder before = RubyTimeOutputFormatter.formatSignedNumber(hours, width, padder);
647657

648658
if (value < 0 && hours == 0) { // the formatter could not handle this case
649-
before = before.toString().replace('+', '-');
659+
for (int i=0; i<before.length(); i++) { // replace('+', '-')
660+
if (before.charAt(i) == '+') before.setCharAt(i, '-');
661+
}
650662
}
651663
return new StringBuilder(before.length() + after.length()).append(before).append(after); // before + after
652664
}

Diff for: ‎core/src/main/java/org/jruby/util/RubyDateParser.java

+54-41
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,12 @@
2525
***** END LICENSE BLOCK *****/
2626
package org.jruby.util;
2727

28-
import org.jruby.Ruby;
29-
import org.jruby.RubyBignum;
30-
import org.jruby.RubyFixnum;
31-
import org.jruby.RubyHash;
32-
import org.jruby.RubyRational;
33-
import org.jruby.RubyString;
34-
import org.jruby.RubySymbol;
28+
import org.jcodings.Encoding;
29+
import org.jruby.*;
3530
import org.jruby.runtime.ThreadContext;
3631
import org.jruby.runtime.builtin.IRubyObject;
3732

33+
import java.math.BigInteger;
3834
import java.util.List;
3935

4036
import static org.jruby.util.StrptimeParser.FormatBag.has;
@@ -43,6 +39,7 @@
4339
* This class has {@code StrptimeParser} and provides methods that are calls from JRuby.
4440
*/
4541
public class RubyDateParser {
42+
4643
/**
4744
* Date._strptime method in JRuby 9.1.5.0's lib/ruby/stdlib/date/format.rb is replaced
4845
* with this method. This is Java implementation of date__strptime method in MRI 2.3.1's
@@ -53,69 +50,85 @@ public class RubyDateParser {
5350
*/
5451

5552
public IRubyObject parse(ThreadContext context, final RubyString format, final RubyString text) {
56-
final List<StrptimeToken> compiledPattern = context.runtime.getCachedStrptimePattern(format.asJavaString());
53+
return parse(context, format.asJavaString(), text);
54+
}
55+
56+
public IRubyObject parse(ThreadContext context, final String format, final RubyString text) {
57+
final List<StrptimeToken> compiledPattern = context.runtime.getCachedStrptimePattern(format);
5758
final StrptimeParser.FormatBag bag = new StrptimeParser().parse(compiledPattern, text.asJavaString());
5859

59-
return bag == null ? context.nil : convertFormatBagToHash(context, bag, text.isTaint());
60+
return bag == null ? context.nil : convertFormatBagToHash(context, bag, text.getEncoding(), text.isTaint());
6061
}
6162

62-
private IRubyObject convertFormatBagToHash(ThreadContext context, StrptimeParser.FormatBag bag, boolean tainted) {
63-
Ruby runtime = context.runtime;
64-
RubyHash hash = RubyHash.newHash(runtime);
65-
66-
if (has(bag.getMDay())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "mday"), runtime.newFixnum(bag.getMDay()));
67-
if (has(bag.getWDay())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "wday"), runtime.newFixnum(bag.getWDay()));
68-
if (has(bag.getCWDay())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "cwday"), runtime.newFixnum(bag.getCWDay()));
69-
if (has(bag.getYDay())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "yday"), runtime.newFixnum(bag.getYDay()));
70-
if (has(bag.getCWeek())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "cweek"), runtime.newFixnum(bag.getCWeek()));
71-
if (has(bag.getCWYear())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "cwyear"), RubyBignum.newBignum(runtime, bag.getCWYear()));
72-
if (has(bag.getMin())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "min"), runtime.newFixnum(bag.getMin()));
73-
if (has(bag.getMon())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "mon"), runtime.newFixnum(bag.getMon()));
74-
if (has(bag.getHour())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "hour"), runtime.newFixnum(bag.getHour()));
75-
if (has(bag.getYear())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "year"), RubyBignum.newBignum(runtime, bag.getYear()));
76-
if (has(bag.getSec())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "sec"), runtime.newFixnum(bag.getSec()))
77-
;
78-
if (has(bag.getWNum0())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "wnum0"), runtime.newFixnum(bag.getWNum0()));
79-
if (has(bag.getWNum1())) hash.op_aset(context, RubySymbol.newSymbol(runtime, "wnum1"), runtime.newFixnum(bag.getWNum1()));
63+
static RubyHash convertFormatBagToHash(ThreadContext context, StrptimeParser.FormatBag bag,
64+
Encoding encoding, boolean tainted) {
65+
final Ruby runtime = context.runtime;
66+
final RubyHash hash = RubyHash.newHash(runtime);
67+
68+
if (has(bag.getMDay())) setHashValue(runtime, hash, "mday", runtime.newFixnum(bag.getMDay()));
69+
if (has(bag.getWDay())) setHashValue(runtime, hash, "wday", runtime.newFixnum(bag.getWDay()));
70+
if (has(bag.getCWDay())) setHashValue(runtime, hash, "cwday", runtime.newFixnum(bag.getCWDay()));
71+
if (has(bag.getYDay())) setHashValue(runtime, hash, "yday", runtime.newFixnum(bag.getYDay()));
72+
if (has(bag.getCWeek())) setHashValue(runtime, hash, "cweek", runtime.newFixnum(bag.getCWeek()));
73+
if (has(bag.getCWYear())) setHashValue(runtime, hash, "cwyear", RubyBignum.newBignum(runtime, bag.getCWYear()));
74+
if (has(bag.getMin())) setHashValue(runtime, hash, "min", runtime.newFixnum(bag.getMin()));
75+
if (has(bag.getMon())) setHashValue(runtime, hash, "mon", runtime.newFixnum(bag.getMon()));
76+
if (has(bag.getHour())) setHashValue(runtime, hash, "hour", runtime.newFixnum(bag.getHour()));
77+
if (has(bag.getYear())) setHashValue(runtime, hash, "year", RubyBignum.newBignum(runtime, bag.getYear()));
78+
if (has(bag.getSec())) setHashValue(runtime, hash, "sec", runtime.newFixnum(bag.getSec()));
79+
if (has(bag.getWNum0())) setHashValue(runtime, hash, "wnum0", runtime.newFixnum(bag.getWNum0()));
80+
if (has(bag.getWNum1())) setHashValue(runtime, hash, "wnum1", runtime.newFixnum(bag.getWNum1()));
8081

8182
if (bag.getZone() != null) {
82-
final RubyString zone = RubyString.newString(runtime, bag.getZone());
83+
final RubyString zone = RubyString.newString(runtime, bag.getZone(), encoding);
8384
if (tainted) zone.taint(context);
8485

85-
hash.op_aset(context, RubySymbol.newSymbol(runtime, "zone"), zone);
86+
setHashValue(runtime, hash, "zone", zone);
8687
int offset = TimeZoneConverter.dateZoneToDiff(bag.getZone());
87-
if (offset != Integer.MIN_VALUE) hash.op_aset(context, RubySymbol.newSymbol(runtime, "offset"), runtime.newFixnum(offset));
88+
if (offset != TimeZoneConverter.INVALID_ZONE) setHashValue(runtime, hash, "offset", runtime.newFixnum(offset));
8889
}
8990

9091
if (has(bag.getSecFraction())) {
91-
final RubyBignum secFraction = RubyBignum.newBignum(runtime, bag.getSecFraction());
92-
final RubyFixnum secFractionSize = RubyFixnum.newFixnum(runtime, (long)Math.pow(10, bag.getSecFractionSize()));
93-
hash.op_aset(context, RubySymbol.newSymbol(runtime, "sec_fraction"),
92+
final RubyInteger secFraction = toRubyInteger(runtime, bag.getSecFraction());
93+
final RubyFixnum secFractionSize = RubyFixnum.newFixnum(runtime, (long) Math.pow(10, bag.getSecFractionSize()));
94+
setHashValue(runtime, hash, "sec_fraction",
9495
RubyRational.newRationalCanonicalize(context, secFraction, secFractionSize));
9596
}
9697

9798
if (bag.has(bag.getSeconds())) {
9899
if (has(bag.getSecondsSize())) {
99-
final RubyBignum seconds = RubyBignum.newBignum(runtime, bag.getSeconds());
100-
final RubyFixnum secondsSize = RubyFixnum.newFixnum(runtime, (long)Math.pow(10, bag.getSecondsSize()));
101-
hash.op_aset(context, RubySymbol.newSymbol(runtime, "seconds"), RubyRational.newRationalCanonicalize(context, seconds, secondsSize));
100+
final RubyInteger seconds = toRubyInteger(runtime, bag.getSeconds());
101+
final RubyFixnum secondsSize = RubyFixnum.newFixnum(runtime, (long) Math.pow(10, bag.getSecondsSize()));
102+
setHashValue(runtime, hash, "seconds", RubyRational.newRationalCanonicalize(context, seconds, secondsSize));
102103
} else {
103-
hash.op_aset(context, RubySymbol.newSymbol(runtime, "seconds"), RubyBignum.newBignum(runtime, bag.getSeconds()));
104+
setHashValue(runtime, hash, "seconds", toRubyInteger(runtime, bag.getSeconds()));
104105
}
105106
}
106107
if (has(bag.getMerid())) {
107-
hash.op_aset(context, RubySymbol.newSymbol(runtime, "_merid"), runtime.newFixnum(bag.getMerid()));
108+
setHashValue(runtime, hash, "_merid", runtime.newFixnum(bag.getMerid()));
108109
}
109110
if (has(bag.getCent())) {
110-
hash.op_aset(context, RubySymbol.newSymbol(runtime, "_cent"), RubyBignum.newBignum(runtime, bag.getCent()));
111+
setHashValue(runtime, hash, "_cent", RubyBignum.newBignum(runtime, bag.getCent()));
111112
}
112113
if (bag.getLeftover() != null) {
113-
final RubyString leftover = RubyString.newString(runtime, bag.getLeftover());
114+
final RubyString leftover = RubyString.newString(runtime, bag.getLeftover(), encoding);
114115
if (tainted) leftover.taint(context);
115116

116-
hash.op_aset(context, RubySymbol.newSymbol(runtime, "leftover"), leftover);
117+
setHashValue(runtime, hash, "leftover", leftover);
117118
}
118119

119120
return hash;
120121
}
122+
123+
private static RubyInteger toRubyInteger(final Ruby runtime, final Number i) {
124+
if (i instanceof BigInteger) {
125+
return RubyBignum.newBignum(runtime, (BigInteger) i);
126+
}
127+
return RubyFixnum.newFixnum(runtime, i.longValue());
128+
}
129+
130+
private static void setHashValue(final Ruby runtime, final RubyHash hash, final String key, final IRubyObject value) {
131+
hash.fastASet(RubySymbol.newSymbol(runtime, key), value);
132+
}
133+
121134
}

Diff for: ‎core/src/main/java/org/jruby/util/RubyTimeOutputFormatter.java

+29-15
Original file line numberDiff line numberDiff line change
@@ -95,41 +95,55 @@ public String format(CharSequence sequence, long value, FieldType type) {
9595
return sequence.toString();
9696
}
9797

98-
static String formatNumber(long value, int width, char padder) {
98+
static CharSequence formatNumber(long value, int width, char padder) {
9999
if (value >= 0 || padder != '0') {
100-
return padding(Long.toString(value), width, padder).toString();
100+
return padding(Long.toString(value), width, padder);
101101
}
102-
return "-" + padding(Long.toString(-value), width - 1, padder);
102+
return padding(new StringBuilder().append('-'), Long.toString(-value), width - 1, padder);
103103
}
104104

105-
static CharSequence formatSignedNumber(long value, int width, char padder) {
105+
static StringBuilder formatSignedNumber(long value, int width, char padder) {
106+
StringBuilder out = new StringBuilder();
106107
if (padder == '0') {
107108
if (value >= 0) {
108-
return "+" + padding(Long.toString(value), width - 1, padder);
109+
return padding(out.append('+'), Long.toString(value), width - 1, padder);
109110
} else {
110-
return "-" + padding(Long.toString(-value), width - 1, padder);
111+
return padding(out.append('-'), Long.toString(-value), width - 1, padder);
111112
}
112113
} else {
113114
if (value >= 0) {
114-
return padding('+' + Long.toString(value), width, padder);
115+
final StringBuilder str = new StringBuilder().append('+').append(Long.toString(value));
116+
return padding(out, str, width, padder);
115117
} else {
116-
return padding(Long.toString(value), width, padder);
118+
return padding(out, Long.toString(value), width, padder);
117119
}
118120
}
119121
}
120122

121123
private static final int SMALLBUF = 100;
122124

123125
private static CharSequence padding(CharSequence sequence, int width, char padder) {
124-
if (sequence.length() >= width) return sequence;
126+
final int len = sequence.length();
127+
if (len >= width) return sequence;
125128

126129
if (width > SMALLBUF) throw new IndexOutOfBoundsException("padding width " + width + " too large");
127130

128-
StringBuilder buf = new StringBuilder(width + sequence.length());
129-
for (int i = sequence.length(); i < width; i++) {
130-
buf.append(padder);
131-
}
132-
buf.append(sequence);
133-
return buf;
131+
StringBuilder out = new StringBuilder(width + len);
132+
for (int i = len; i < width; i++) out.append(padder);
133+
out.append(sequence);
134+
return out;
134135
}
136+
137+
private static StringBuilder padding(final StringBuilder out, CharSequence sequence,
138+
final int width, final char padder) {
139+
final int len = sequence.length();
140+
if (len >= width) return out.append(sequence);
141+
142+
if (width > SMALLBUF) throw new IndexOutOfBoundsException("padding width " + width + " too large");
143+
144+
out.ensureCapacity(width + len);
145+
for (int i = len; i < width; i++) out.append(padder);
146+
return out.append(sequence);
147+
}
148+
135149
}

Diff for: ‎core/src/main/java/org/jruby/util/StrptimeParser.java

+47-34
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,16 @@ public static class FormatBag {
8282

8383
private String zone = null;
8484

85-
private BigInteger secFraction = null; // Rational
85+
private Number secFraction = null; // Rational
8686
private int secFractionSize = Integer.MIN_VALUE;
8787

88-
private BigInteger seconds = null; // Bignum or Rational
88+
private Number seconds = null; // Bignum or Rational
8989
private int secondsSize = Integer.MIN_VALUE;
9090

9191
private int merid = Integer.MIN_VALUE;
9292
private long cent = Long.MIN_VALUE;
9393

94-
private boolean fail = false;
94+
//private boolean fail = false;
9595
private String leftover = null;
9696

9797
public int getMDay() {
@@ -150,15 +150,15 @@ public String getZone() {
150150
return zone;
151151
}
152152

153-
public BigInteger getSecFraction() {
153+
public Number getSecFraction() {
154154
return secFraction;
155155
}
156156

157157
public int getSecFractionSize() {
158158
return secFractionSize;
159159
}
160160

161-
public BigInteger getSeconds() {
161+
public Number getSeconds() {
162162
return seconds;
163163
}
164164

@@ -174,10 +174,6 @@ public long getCent() {
174174
return cent;
175175
}
176176

177-
void fail() {
178-
fail = true;
179-
}
180-
181177
public String getLeftover() {
182178
return leftover;
183179
}
@@ -190,7 +186,7 @@ public static boolean has(long v) {
190186
return v != Long.MIN_VALUE;
191187
}
192188

193-
public static boolean has(BigInteger v) {
189+
public static boolean has(Number v) {
194190
return v != null;
195191
}
196192
}
@@ -465,7 +461,7 @@ private FormatBag parse(final List<StrptimeToken> compiledPattern) {
465461
pos++;
466462
}
467463

468-
final BigInteger v;
464+
final Number v;
469465
final int initPos = pos;
470466
if (isNumberPattern(compiledPattern, tokenIndex)) {
471467
if (token.getFormat() == StrptimeFormat.FORMAT_MILLISEC) {
@@ -477,7 +473,7 @@ private FormatBag parse(final List<StrptimeToken> compiledPattern) {
477473
v = readDigitsMax();
478474
}
479475

480-
bag.secFraction = !negative ? v : v.negate();
476+
bag.secFraction = !negative ? v : negateInteger(v);
481477
bag.secFractionSize = pos - initPos;
482478
break;
483479
}
@@ -515,8 +511,8 @@ private FormatBag parse(final List<StrptimeToken> compiledPattern) {
515511
pos++;
516512
}
517513

518-
final BigInteger sec = readDigitsMax();
519-
bag.seconds = !negative ? sec : sec.negate();
514+
final Number sec = readDigitsMax();
515+
bag.seconds = !negative ? sec : negateInteger(sec);
520516
bag.secondsSize = 3;
521517
break;
522518
}
@@ -535,8 +531,8 @@ private FormatBag parse(final List<StrptimeToken> compiledPattern) {
535531
pos++;
536532
}
537533

538-
final BigInteger sec = readDigitsMax();
539-
bag.seconds = !negative ? sec : sec.negate();
534+
final Number sec = readDigitsMax();
535+
bag.seconds = !negative ? sec : negateInteger(sec);
540536
break;
541537
}
542538
case FORMAT_WEEK_YEAR_S: // %U, %OU - Week number of the year. The week starts with Sunday. (00..53)
@@ -680,31 +676,43 @@ private long readDigits(final int len) {
680676
/**
681677
* Ports READ_DIGITS_MAX from ext/date/date_strptime.c in MRI 2.3.1 under BSDL.
682678
* see https://github.com/ruby/ruby/blob/394fa89c67722d35bdda89f10c7de5c304a5efb1/ext/date/date_strftime.c
679+
*
680+
* @return integer value (Long or BigInteger)
683681
*/
684-
private BigInteger readDigitsMax() {
682+
private Number readDigitsMax() {
685683
char c;
686-
BigInteger v = BigInteger.ZERO;
684+
long v = 0; BigInteger vBig = null;
687685
final int initPos = pos;
688686

689687
while (true) {
690-
if (isEndOfText(text, pos)) {
691-
break;
692-
}
688+
if (isEndOfText(text, pos)) break;
693689

694690
c = text.charAt(pos);
695-
if (!isDigit(c)) {
696-
break;
697-
} else {
698-
v = v.multiply(BigInteger.TEN).add(new BigInteger(Integer.toString(toInt(c))));
691+
if (!isDigit(c)) break;
692+
693+
if (vBig == null) {
694+
try {
695+
long tmp = Math.multiplyExact(v, 10);
696+
tmp = Math.addExact(tmp, toInt(c));
697+
v = tmp;
698+
}
699+
catch (ArithmeticException overflow) {
700+
vBig = BigInteger.valueOf(v); continue;
701+
}
699702
}
703+
else {
704+
vBig = vBig.multiply(BigInteger.TEN);
705+
vBig = vBig.add(BigInteger.valueOf(toInt(c)));
706+
}
707+
700708
pos += 1;
701709
}
702710

703711
if (pos == initPos) {
704712
fail = true;
705713
}
706714

707-
return v;
715+
return vBig == null ? v : vBig;
708716
}
709717

710718
private long readDigitsMaxLong() {
@@ -713,16 +721,13 @@ private long readDigitsMaxLong() {
713721
final int initPos = pos;
714722

715723
while (true) {
716-
if (isEndOfText(text, pos)) {
717-
break;
718-
}
724+
if (isEndOfText(text, pos)) break;
719725

720726
c = text.charAt(pos);
721-
if (!isDigit(c)) {
722-
break;
723-
} else {
724-
v = v * 10 + toInt(c);
725-
}
727+
if (!isDigit(c)) break;
728+
729+
v = v * 10 + toInt(c);
730+
726731
pos += 1;
727732
}
728733

@@ -843,5 +848,13 @@ private static boolean isBlank(String text, int pos) {
843848
private static int toInt(char c) {
844849
return c - '0';
845850
}
851+
852+
private static Number negateInteger(final Number i) {
853+
if (i instanceof BigInteger) {
854+
return ((BigInteger) i).negate();
855+
}
856+
return -i.longValue();
857+
}
858+
846859
}
847860
}

Diff for: ‎core/src/main/java/org/jruby/util/TimeZoneConverter.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ private static int getOffsetFromZonesSource(String z) {
381381
}
382382
}
383383

384+
public static final int INVALID_ZONE = Integer.MIN_VALUE;
385+
384386
/**
385387
* Ports date_zone_to_diff from ext/date/date_parse.c in MRI 2.3.1 under BSDL.
386388
*/
@@ -420,7 +422,7 @@ public static int dateZoneToDiff(String zone) {
420422
sign = false;
421423
} else {
422424
// if z doesn't start with "+" or "-", invalid
423-
return Integer.MIN_VALUE;
425+
return INVALID_ZONE;
424426
}
425427
z = z.substring(1);
426428

Diff for: ‎lib/ruby/stdlib/date.rb

+54-1,223
Large diffs are not rendered by default.

Diff for: ‎lib/ruby/stdlib/date/format.rb

+13-314
Large diffs are not rendered by default.

Diff for: ‎spec/tags/ruby/library/date/minus_month_tags.txt

-1
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
fails:Date#<< raises an error on non numeric parameters

Diff for: ‎spec/tags/ruby/library/date/valid_jd_tags.txt

-1
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
fails:Date.valid_jd? returns true if passed false

Diff for: ‎spec/tags/ruby/library/datetime/hour_tags.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
fails:DateTime#hour raises an error for Rational
22
fails:DateTime#hour raises an error for Float
3+
fails:DateTime#hour raises an error for hour fractions smaller than -24

Diff for: ‎spec/tags/ruby/library/datetime/min_tags.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
fails:DateTime.min raises an error for Rational
22
fails:DateTime.min raises an error for Float
3+
fails:DateTime.min raises an error for minute fractions smaller than -60

Diff for: ‎spec/tags/ruby/library/datetime/minute_tags.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
fails:DateTime.minute raises an error for Rational
22
fails:DateTime.minute raises an error for Float
3+
fails:DateTime.minute raises an error for minute fractions smaller than -60

Diff for: ‎spec/tags/ruby/library/datetime/sec_tags.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
fails:DateTime.sec raises an error when minute is given as a rational
2+
fails:DateTime.sec raises an error for second fractions smaller than -60

Diff for: ‎spec/tags/ruby/library/datetime/second_tags.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
fails:DateTime#second raises an error when minute is given as a rational
2+
fails:DateTime#second raises an error for second fractions smaller than -60

Diff for: ‎test/jruby.index

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jruby/test_comparable
1717
jruby/test_core_arities
1818
jruby/test_custom_enumerable
1919
jruby/test_cvars_in_odd_scopes
20-
jruby/test_date_joda_time
20+
jruby/test_date
2121
jruby/test_defined
2222
jruby/test_default_constants
2323
jruby/test_delegated_array_equals

Diff for: ‎test/jruby/test_date.rb

+747
Large diffs are not rendered by default.

Diff for: ‎test/jruby/test_date_joda_time.rb

-15
This file was deleted.

Diff for: ‎test/mri/excludes/TestDateStrftime.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
exclude :test_strftime__gnuext_complex, "need important implem changes and almost useless feature"
2-
exclude :test_strftime__offset, ""
2+
exclude :test_strftime__offset, "works with (-23..23) +24:00 and -24:00 'virtual' offsets not supported"

Diff for: ‎test/mri/excludes/TestSH.rb

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
1-
exclude :test_enc, "depending on MRI buffer sizes, not useful"
2-
exclude :test_inspect, "depending on MRI buffer sizes, not useful"
3-
exclude :test_period2, ""
1+
exclude :test_period2, "bignum too big to convert into `long'" # not relevant - JODA doesn't handle huge years anyway
42
exclude :test_strftime, "depending on MRI buffer sizes, not useful"
5-
exclude :test_to_s, "depending on MRI buffer sizes, not useful"
6-
exclude :test_zone, "depending on MRI buffer sizes, not useful"

0 commit comments

Comments
 (0)
Please sign in to comment.