Skip to content

Commit

Permalink
Merge pull request #297 from implydata/accept-time-bounds
Browse files Browse the repository at this point in the history
Accept time bounds for TimeRangeExpressions and TimeBucketExpressions
  • Loading branch information
lorem--ipsum committed Sep 20, 2023
2 parents 53bd4c3 + 492b989 commit 0a6bb96
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 10 deletions.
8 changes: 8 additions & 0 deletions src/datatypes/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export interface PlywoodRangeJS {
export abstract class Range<T> {
static DEFAULT_BOUNDS = '[)';

static areEquivalentBounds(bounds1: string | undefined, bounds2: string | undefined): boolean {
return (
bounds1 === bounds2 ||
(!bounds1 && bounds2 === Range.DEFAULT_BOUNDS) ||
(!bounds2 && bounds1 === Range.DEFAULT_BOUNDS)
);
}

static isRange(candidate: any): candidate is PlywoodRange {
return candidate instanceof Range;
}
Expand Down
16 changes: 14 additions & 2 deletions src/datatypes/timeRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,19 @@ export class TimeRange extends Range<Date> implements Instance<TimeRangeValue, T
return dateToIntervalPart(date) + '/' + dateToIntervalPart(new Date(date.valueOf() + 1));
}

static timeBucket(date: Date, duration: Duration, timezone: Timezone): TimeRange {
static timeBucket(
date: Date,
duration: Duration,
timezone: Timezone,
bounds?: string,
): TimeRange {
if (!date) return null;
const start = duration.floor(date, timezone);

return new TimeRange({
start: start,
end: duration.shift(start, timezone, 1),
bounds: Range.DEFAULT_BOUNDS,
bounds: bounds ?? Range.DEFAULT_BOUNDS,
});
}

Expand Down Expand Up @@ -183,6 +189,12 @@ export class TimeRange extends Range<Date> implements Instance<TimeRangeValue, T
});
}

public changeBounds(bounds: string): TimeRange {
const value = this.toJS();
value.bounds = bounds;
return TimeRange.fromJS(value);
}

public shift(duration: Duration, timezone: Timezone, step?: number): TimeRange {
const { start, end, bounds } = this;
if (!start) return this;
Expand Down
2 changes: 2 additions & 0 deletions src/expressions/baseExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export interface ExpressionValue {
mvArray?: string[];
ipToSearch?: Ip;
ipSearchType?: string;
bounds?: string;
}

export interface ExpressionJS {
Expand Down Expand Up @@ -290,6 +291,7 @@ export interface ExpressionJS {
mvArray?: string[];
ipToSearch?: Ip;
ipSearchType?: string;
bounds?: string;
}

export interface ExtractAndRest {
Expand Down
17 changes: 15 additions & 2 deletions src/expressions/timeBucketExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { Duration, Timezone } from 'chronoshift';
import { immutableEqual } from 'immutable-class';

import { PlywoodValue } from '../datatypes';
import { PlywoodValue, Range } from '../datatypes';
import { TimeRange } from '../datatypes/timeRange';
import { SQLDialect } from '../dialect/baseDialect';

Expand All @@ -30,17 +30,20 @@ export class TimeBucketExpression extends ChainableExpression {
const value = ChainableExpression.jsToValue(parameters);
value.duration = Duration.fromJS(parameters.duration);
if (parameters.timezone) value.timezone = Timezone.fromJS(parameters.timezone);
if (parameters.bounds) value.bounds = parameters.bounds;
return new TimeBucketExpression(value);
}

public duration: Duration;
public timezone: Timezone;
public bounds: string;

constructor(parameters: ExpressionValue) {
super(parameters, dummyObject);
const duration = parameters.duration;
this.duration = duration;
this.timezone = parameters.timezone;
this.bounds = parameters.bounds;
this._ensureOp('timeBucket');
this._checkOperandTypes('TIME');
if (!(duration instanceof Duration)) {
Expand All @@ -55,13 +58,15 @@ export class TimeBucketExpression extends ChainableExpression {
public valueOf(): ExpressionValue {
const value = super.valueOf();
value.duration = this.duration;
value.bounds = this.bounds;
if (this.timezone) value.timezone = this.timezone;
return value;
}

public toJS(): ExpressionJS {
const js = super.toJS();
js.duration = this.duration.toJS();
if (this.bounds) js.bounds = this.bounds;
if (this.timezone) js.timezone = this.timezone.toJS();
return js;
}
Expand All @@ -70,26 +75,34 @@ export class TimeBucketExpression extends ChainableExpression {
return (
super.equals(other) &&
this.duration.equals(other.duration) &&
Range.areEquivalentBounds(this.bounds, other.bounds) &&
immutableEqual(this.timezone, other.timezone)
);
}

protected _toStringParameters(_indent?: int): string[] {
const ret = [this.duration.toString()];
if (this.timezone) ret.push(Expression.safeString(this.timezone.toString()));
if (this.bounds) ret.push(this.bounds);
return ret;
}

protected _calcChainableHelper(operandValue: any): PlywoodValue {
return operandValue
? TimeRange.timeBucket(operandValue, this.duration, this.getTimezone())
? TimeRange.timeBucket(operandValue, this.duration, this.getTimezone(), this.bounds)
: null;
}

protected _getSQLChainableHelper(dialect: SQLDialect, operandSQL: string): string {
return dialect.timeBucketExpression(operandSQL, this.duration, this.getTimezone());
}

public changeBounds(bounds: string): Expression {
const value = this.valueOf();
value.bounds = bounds;
return Expression.fromValue(value);
}

// HasTimezone mixin:
public getTimezone: () => Timezone;
public changeTimezone: (timezone: Timezone) => this;
Expand Down
18 changes: 15 additions & 3 deletions src/expressions/timeRangeExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { Duration, Timezone } from 'chronoshift';
import { immutableEqual } from 'immutable-class';

import { PlywoodValue } from '../datatypes';
import { PlywoodValue, Range } from '../datatypes';
import { TimeRange } from '../datatypes/timeRange';
import { SQLDialect } from '../dialect/baseDialect';
import { pluralIfNeeded } from '../helper/utils';
Expand All @@ -33,19 +33,22 @@ export class TimeRangeExpression extends ChainableExpression implements HasTimez
const value = ChainableExpression.jsToValue(parameters);
value.duration = Duration.fromJS(parameters.duration);
value.step = parameters.step;
value.bounds = parameters.bounds;
if (parameters.timezone) value.timezone = Timezone.fromJS(parameters.timezone);
return new TimeRangeExpression(value);
}

public duration: Duration;
public step: number;
public timezone: Timezone;
public bounds: string;

constructor(parameters: ExpressionValue) {
super(parameters, dummyObject);
this.duration = parameters.duration;
this.step = parameters.step || TimeRangeExpression.DEFAULT_STEP;
this.timezone = parameters.timezone;
this.bounds = parameters.bounds;
this._ensureOp('timeRange');
this._checkOperandTypes('TIME');
if (!(this.duration instanceof Duration)) {
Expand All @@ -59,6 +62,7 @@ export class TimeRangeExpression extends ChainableExpression implements HasTimez
value.duration = this.duration;
value.step = this.step;
if (this.timezone) value.timezone = this.timezone;
if (this.bounds) value.bounds = this.bounds;
return value;
}

Expand All @@ -67,6 +71,7 @@ export class TimeRangeExpression extends ChainableExpression implements HasTimez
js.duration = this.duration.toJS();
js.step = this.step;
if (this.timezone) js.timezone = this.timezone.toJS();
if (this.bounds) js.bounds = this.bounds;
return js;
}

Expand All @@ -75,6 +80,7 @@ export class TimeRangeExpression extends ChainableExpression implements HasTimez
super.equals(other) &&
this.duration.equals(other.duration) &&
this.step === other.step &&
Range.areEquivalentBounds(this.bounds, other.bounds) &&
immutableEqual(this.timezone, other.timezone)
);
}
Expand All @@ -99,16 +105,22 @@ export class TimeRangeExpression extends ChainableExpression implements HasTimez
if (operandValue === null) return null;
const other = duration.shift(operandValue, timezone, step);
if (step > 0) {
return new TimeRange({ start: operandValue, end: other });
return new TimeRange({ start: operandValue, end: other, bounds: this.bounds });
} else {
return new TimeRange({ start: other, end: operandValue });
return new TimeRange({ start: other, end: operandValue, bounds: this.bounds });
}
}

protected _getSQLChainableHelper(_dialect: SQLDialect, _operandSQL: string): string {
throw new Error('implement me');
}

public changeBounds(bounds: string): Expression {
const value = this.valueOf();
value.bounds = bounds;
return Expression.fromValue(value);
}

// HasTimezone mixin:
public getTimezone: () => Timezone;
public changeTimezone: (timezone: Timezone) => TimeRangeExpression;
Expand Down
51 changes: 48 additions & 3 deletions test/datatypes/timeRange.mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ const { expect } = require('chai');

const { testImmutableClass } = require('immutable-class-tester');

const { Timezone } = require('chronoshift');
const { Timezone, Duration } = require('chronoshift');
const plywood = require('../plywood');

const { TimeRange, $, ply, r } = plywood;
const { Range, TimeRange } = plywood;

describe('TimeRange', () => {
it('is immutable class', () => {
Expand Down Expand Up @@ -228,4 +227,50 @@ describe('TimeRange', () => {
});
});
});

describe('Accepts bounds', () => {
it('Can create a Time Range from a time bucket with bounds', () => {
const timeRange = TimeRange.timeBucket(
new Date('2015-02-26T05:00:00.000Z'),
Duration.fromJS('PT1H'),
Timezone.fromJS('Etc/UTC'),
'[]',
);
expect(timeRange.toJS()).to.deep.equal({
start: new Date('2015-02-26T05:00:00.000Z'),
end: new Date('2015-02-26T06:00:00.000Z'),
bounds: '[]',
});
});

it('Can create a Time Range from a time bucket without explicit bounds', () => {
const timeRange = TimeRange.timeBucket(
new Date('2015-02-26T05:00:00.000Z'),
Duration.fromJS('PT1H'),
Timezone.fromJS('Etc/UTC'),
);

// does not include bounds in toJS if default bounds are used
expect(timeRange.toJS()).to.deep.equal({
start: new Date('2015-02-26T05:00:00.000Z'),
end: new Date('2015-02-26T06:00:00.000Z'),
});
expect(timeRange.bounds).to.equal(Range.DEFAULT_BOUNDS);
});

it('Can change bounds', () => {
const timeRange = TimeRange.timeBucket(
new Date('2015-02-26T05:00:00.000Z'),
Duration.fromJS('PT1H'),
Timezone.fromJS('Etc/UTC'),
);

// does not include bounds in toJS if default bounds are used
expect(timeRange.changeBounds('()').toJS()).to.deep.equal({
start: new Date('2015-02-26T05:00:00.000Z'),
end: new Date('2015-02-26T06:00:00.000Z'),
bounds: '()',
});
});
});
});
16 changes: 16 additions & 0 deletions test/expression/expression.mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ describe('Expression', () => {
{ op: 'timeBucket', duration: 'P1D' },
{ op: 'timeBucket', duration: 'PT2H', timezone: 'Etc/UTC' },
{ op: 'timeBucket', duration: 'PT2H', timezone: 'America/Los_Angeles' },
{ op: 'timeBucket', duration: 'PT2H', timezone: 'America/Los_Angeles', bounds: '[]' },
{ op: 'timeBucket', duration: 'PT3H', timezone: 'America/Los_Angeles', bounds: '[)' },

{ op: 'timePart', part: 'DAY_OF_WEEK' },
{ op: 'timePart', part: 'DAY_OF_MONTH', timezone: 'Etc/UTC' },
Expand All @@ -243,6 +245,20 @@ describe('Expression', () => {
{ op: 'timeRange', duration: 'P1D', step: -2 },
{ op: 'timeRange', duration: 'P2D', step: 3, timezone: 'Etc/UTC' },
{ op: 'timeRange', duration: 'P2D', step: 3, timezone: 'America/Los_Angeles' },
{
op: 'timeRange',
duration: 'P2D',
step: 3,
timezone: 'America/Los_Angeles',
bounds: '[]',
},
{
op: 'timeRange',
duration: 'P2D',
step: 4,
timezone: 'America/Los_Angeles',
bounds: '[)',
},

{ op: 'transformCase', transformType: 'upperCase' },
{ op: 'transformCase', transformType: 'lowerCase' },
Expand Down
73 changes: 73 additions & 0 deletions test/expression/timeBucketExpression.mocha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2015-2020 Imply Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const { expect } = require('chai');

const plywood = require('../plywood');
const { TimeBucketExpression } = plywood;

describe('TimeBucketExpression', () => {
const timeBucketWithoutBounds = TimeBucketExpression.fromJS({
duration: 'PT1H',
step: -2,
timezone: 'Etc/UTC',
});

describe('Accepts bounds', () => {
it('Can create an expression with bounds', () => {
const timeBucket = TimeBucketExpression.fromJS({
duration: 'PT1H',
timezone: 'Etc/UTC',
bounds: '[]',
});
expect(timeBucket.toJS()).to.deep.equal({
duration: 'PT1H',
timezone: 'Etc/UTC',
bounds: '[]',
op: 'timeBucket',
});
});

it('Can create an expression without bounds', () => {
expect(timeBucketWithoutBounds.toJS()).to.deep.equal({
duration: 'PT1H',
timezone: 'Etc/UTC',
op: 'timeBucket',
});
});

it('Can change bounds', () => {
expect(timeBucketWithoutBounds.changeBounds('(]').toJS()).to.deep.equal({
duration: 'PT1H',
timezone: 'Etc/UTC',
op: 'timeBucket',
bounds: '(]',
});
});

it('preforms equals properly with bounds', () => {
expect(timeBucketWithoutBounds.equals(timeBucketWithoutBounds.changeBounds('[)'))).to.equal(
true,
);
expect(timeBucketWithoutBounds.equals(timeBucketWithoutBounds.changeBounds('[]'))).to.equal(
false,
);
expect(timeBucketWithoutBounds.changeBounds('[)').equals(timeBucketWithoutBounds)).to.equal(
true,
);
});
});
});
Loading

0 comments on commit 0a6bb96

Please sign in to comment.