<commit_before>
import PureComponent from '../PureComponent';
var React = require('react');
import Input from 'react-toolbox/lib/input';
import {IconMenu, Menu, MenuItem} from 'react-toolbox/lib/menu';
var classNames = require('classnames');
var Rx = require('../rx').default;
import Tooltip from 'react-toolbox/lib/tooltip';

var TooltipInput = Tooltip(({inputRef, ...props}) =>
		<Input innerRef={inputRef} {...props}/>);
var TooltipI = Tooltip('i');

// The Tooltip component will get mouse enter/leave events on menu children,
// causing the tooltip to display in front of the menu when it is open. There's
// no way to disable the Tooltip while the menu is open. This is a workaround
// that intercepts the onMouseEnter method that is injected by the Tooltip
// component, and filters events based on the event target type.
class IconMenuWrapper extends PureComponent {
	onMouseEnter = ev => {
		if (ev.target.type === 'button') {
			this.props.onMouseEnter(ev);
		}
	}
	render() {
		var {onMouseEnter, ...props} = this.props; //eslint-disable-line no-unused-vars
		return <IconMenu onMouseEnter={this.onMouseEnter} {...props}/>;
	}
}

var TooltipIconMenu = Tooltip(IconMenuWrapper);

// Styles
var compStyles = require('./SampleSearch.module.css');

//var SampleIDInput = React.createClass({
//	getInitialState() {
//		return {
//			show: false,
//			value: undefined
//		};
//	},
//	onChange (ev) {
//		var value = ev.target.value;
//		this.setState({value});
//	},
//	close () {
//		this.setState({ show: false});
//	},
//	submit () {
//		var samplesList = this.state.value.split(/\s+/),
//			{onSearchIDAndFilterColumn, onSamplesSubmit} = this.props;
//		this.close();
//		this.state.value = '';
//		onSearchIDAndFilterColumn(samplesList);
//		onSamplesSubmit("A:true"); // since the new column is always column A
//	},
//	render() {
//		var tooltipButton = <Tooltip>Search by sample IDs</Tooltip>,
//			tooltipModalString = 'Samples match your list will be highlighted. \
//				A new binary column will be made using your list: matched samples vs. the rest.',
//			tooltipModal = <Tooltip>{tooltipModalString}</Tooltip>,
//			{cohortSamples, disabled} = this.props,
//			help = 'e.g.\n' + Object.values(cohortSamples)[0].slice(0, 5).join('\n') + '\n...';
//
//		return (
//
//				<OverlayTrigger trigger={['hover']} placement="top" overlay={tooltipButton}>
//					<Button
//						bsSize = "small"
//						onClick = {() => this.setState({ show: true})}
//						disabled={disabled}>
//						Custom Sample List
//					</Button>
//				</OverlayTrigger>
//				<Modal
//					show={this.state.show}
//					onHide={this.close}
//					container={this}
//					aria-labelledby="contained-modal-title">
//					<Modal.Header closeButton>
//						<Modal.Title id="contained-modal-title">
//							Enter a list of samaple IDs to highlight
//							<OverlayTrigger trigger={['hover']} placement="right" overlay={tooltipModal}>
//
//							</OverlayTrigger>
//						</Modal.Title>
//					</Modal.Header>
//					<Modal.Body>
//						<Input style={{width: 550, height: 200}}
//							value={this.state.value}
//							type ="textarea"
//							placeholder={help}
//							onChange={this.onChange}/>
//					</Modal.Body>
//					<Modal.Footer>
//						<Button bsStyle="primary" onClick={this.submit} disabled={!this.state.value}>Submit</Button>
//						<Button onClick={this.close}>Cancel</Button>
//					</Modal.Footer>
//				</Modal>
//
//		);
//	}
//});

function findSubExpr(offsets, i) {
	var j = 0;
	while (i > offsets[j]) {
		++j;
	}
	return j;
}
var tooltips = {
	filter: 'Filter samples',
	subgroup: 'Create sample subgroups from data on the screen',
	find: 'Highlight or zoom in on a selection of samples',
	clear: 'Clear current filter and show all samples in the study',
	input: 'Examples: "missense", "null","B:>2"',
	keep: 'Keep only the selected samples on the screen',
	remove: 'Remove the selected samples from the screen',
	makeSubgroup: 'Matched samples will be one subgroup and non-matched samples will be the other subgroup',
	highlight: 'Highlight selected samples',
	zoom: 'Zoom in to selected samples',
	history: 'Previous search text'
};

var placeholder = {
	true: 'Click on visual spreadsheet or type here to select samples',
	false: 'Type here to select samples'
};

var input = comp => {
	var {matches, mode, history, pickSamples} = comp.props,
		{value} = comp.state,
		hasHistory = history.length > 0,
		noshow = (mode !== "heatmap");

	return (<TooltipInput key='input'
			className={classNames(compStyles.inputContainer, pickSamples && compStyles.picking)}
			tooltip={tooltips.input}
			onKeyUp={comp.onCaret}
			onClick={comp.onCaret}
			onFocus={comp.onCaret}
			onBlur={comp.onHideCaret}
			inputRef={comp.setRef}
			spellCheck={false}
			type='text'
			value={value || ''}
			placeholder={placeholder[pickSamples]}
			onChange={comp.onChange}
			disabled={noshow}>
		<TooltipI tooltip={tooltips.history} onClick={comp.onOpenHistory}
				className={classNames(compStyles.dropDownArrow,
					hasHistory && compStyles.hasHistory, 'material-icons')}>
				arrow_drop_down</TooltipI>

		<Menu theme={{static: compStyles.history, active: compStyles.historyActive}}
				onShow={comp.onShowHistory} onHide={comp.onHideHistory}
				position='static' active={comp.state.historyOpen}>
			{history.map((b, i) =>
					// Note that Menu onSelect is broken in dev because of component
					// identity comparisons, and MenuItem events are broken in prod
					// because of RT deferring callbacks with a setState call, thus
					// allowing currentTarget to be nulled, so our only option here
					// is to create closures on every render. Very annoying.
					<MenuItem className={compStyles.menuItem} onClick={() => comp.onHistory(b)}
						data-value={b} key={i} value={b} caption={b}/>)}
		</Menu>
		{`${matches} matching samples`}
	</TooltipInput>);
};

export class SampleSearch extends PureComponent {
	state = {value: this.props.value};

	componentDidMount() {
		var highlight = new Rx.Subject();
		this.highlight = highlight.next.bind(highlight);
		this.sub = highlight.startWith(undefined).distinctUntilChanged()
			.subscribe(hl => this.props.onHighlightSelect(hl));
	}

	componentWillUnmount() {
		this.sub.unsubscribe();
	}

	componentWillReceiveProps(newProps) {
		if (this.state.value === this.props.value) {
			this.setState({value: newProps.value});
		}
		// otherwise we have buffered changes to state, and
		// updating from props would revert the user input
		// and move the carat to the end.
	}

	componentDidUpdate() {
		var oldSearch = this.props.oldSearch;
		// oldSearch will update before value, so we have to check for both.
		if (oldSearch != null && this.props.value) {
			// We have to set focus for the selection to be visible. If this
			// is problematic, we'll need to implement our own selection highlight
			// until the element has focus.
			this.input.focus({preventScroll: true});
			this.input.setSelectionRange(oldSearch.length, this.props.value.length);
			this.input.scrollLeft = this.input.scrollWidth;
		}
	}

	onChange = (value) => {
		var {onChange} = this.props;
		this.setState({value});
		onChange(value);
	};

	onSamplesSubmit = (value) => {
		var {onChange} = this.props;
		this.setState({value});
		onChange(value);
	};

	setRef = ref => {
		this.input = ref && ref.inputNode;
	}

	onCaret = () => {
		var hl = this.input && this.props.offsets ?
			findSubExpr(this.props.offsets, this.input.selectionStart) :
			undefined;
		this.highlight(hl);
	}

	onHideCaret = () => {
		this.highlight(undefined);
	}

	onOpenHistory = () => {
		this.setState({historyOpen: !this.state.historyOpen});
	}

	onHistory = value => {
		this.setState({historyOpen: false});
		this.props.onHistory(value); // move to front of history
		this.props.onChange(value);
	}

	// These two are hacks to keep local state synced with the menu, e.g.
	// when the user closes it by clicking outside the menu.
	onShowHistory = () => {
		this.setState({historyOpen: true});
	}

	onHideHistory = () => {
		this.setState({historyOpen: false});
	}

	onRemove = () => {
		this.props.onHistory(this.state.value.trim());
		this.props.onFilter(true);
	}

	onKeep = () => {
		this.props.onHistory(this.state.value.trim());
		this.props.onFilter(false);
	}

	onCreateColumn = () => {
		// have to add to history before the columns reorder, to
		// prevent the history having wrong column ids.
		this.props.onHistory(this.state.value.trim());
		this.props.onCreateColumn();
	}

	onZoom = () => {
		this.props.onHistory(this.state.value.trim());
		this.props.onZoom();
	}

	render() {
		var {matches, sampleCount, mode, pickSamples, onPickSamples, onResetSampleFilter} = this.props,
			disableActions = !(matches > 0 && matches < sampleCount),
			noshow = (mode !== "heatmap");
		return (

				{input(this)}
				<TooltipI className={classNames('material-icons', pickSamples ? compStyles.dark : '')}
					onClick={onPickSamples} tooltip='Pick samples'>colorize</TooltipI>
				{noshow ? filter_list :
				<TooltipIconMenu tooltip='Filter actions' className={compStyles.filterMenu} icon='filter_list' iconRipple={false} position='topLeft'>
					<MenuItem disabled={disableActions} caption='Keep samples' onClick={this.onKeep}/>
					<MenuItem disabled={disableActions} caption='Remove samples' onClick={this.onRemove}/>
					<MenuItem caption='Clear Filter' onClick={onResetSampleFilter}/>
					<MenuItem disabled={disableActions} caption='Zoom' onClick={this.onZoom}/>
					<MenuItem disabled={disableActions} caption='New Column' onClick={this.onCreateColumn}/>
				</TooltipIconMenu>}

		);
	}
}
<commit_msg>Disable dropdown if history is empty<commit_after>
import PureComponent from '../PureComponent';
var React = require('react');
import Input from 'react-toolbox/lib/input';
import {IconMenu, Menu, MenuItem} from 'react-toolbox/lib/menu';
var classNames = require('classnames');
var Rx = require('../rx').default;
import Tooltip from 'react-toolbox/lib/tooltip';

var TooltipInput = Tooltip(({inputRef, ...props}) =>
		<Input innerRef={inputRef} {...props}/>);
var TooltipI = Tooltip('i');

// The Tooltip component will get mouse enter/leave events on menu children,
// causing the tooltip to display in front of the menu when it is open. There's
// no way to disable the Tooltip while the menu is open. This is a workaround
// that intercepts the onMouseEnter method that is injected by the Tooltip
// component, and filters events based on the event target type.
class IconMenuWrapper extends PureComponent {
	onMouseEnter = ev => {
		if (ev.target.type === 'button') {
			this.props.onMouseEnter(ev);
		}
	}
	render() {
		var {onMouseEnter, ...props} = this.props; //eslint-disable-line no-unused-vars
		return <IconMenu onMouseEnter={this.onMouseEnter} {...props}/>;
	}
}

var TooltipIconMenu = Tooltip(IconMenuWrapper);

// Styles
var compStyles = require('./SampleSearch.module.css');

//var SampleIDInput = React.createClass({
//	getInitialState() {
//		return {
//			show: false,
//			value: undefined
//		};
//	},
//	onChange (ev) {
//		var value = ev.target.value;
//		this.setState({value});
//	},
//	close () {
//		this.setState({ show: false});
//	},
//	submit () {
//		var samplesList = this.state.value.split(/\s+/),
//			{onSearchIDAndFilterColumn, onSamplesSubmit} = this.props;
//		this.close();
//		this.state.value = '';
//		onSearchIDAndFilterColumn(samplesList);
//		onSamplesSubmit("A:true"); // since the new column is always column A
//	},
//	render() {
//		var tooltipButton = <Tooltip>Search by sample IDs</Tooltip>,
//			tooltipModalString = 'Samples match your list will be highlighted. \
//				A new binary column will be made using your list: matched samples vs. the rest.',
//			tooltipModal = <Tooltip>{tooltipModalString}</Tooltip>,
//			{cohortSamples, disabled} = this.props,
//			help = 'e.g.\n' + Object.values(cohortSamples)[0].slice(0, 5).join('\n') + '\n...';
//
//		return (
//
//				<OverlayTrigger trigger={['hover']} placement="top" overlay={tooltipButton}>
//					<Button
//						bsSize = "small"
//						onClick = {() => this.setState({ show: true})}
//						disabled={disabled}>
//						Custom Sample List
//					</Button>
//				</OverlayTrigger>
//				<Modal
//					show={this.state.show}
//					onHide={this.close}
//					container={this}
//					aria-labelledby="contained-modal-title">
//					<Modal.Header closeButton>
//						<Modal.Title id="contained-modal-title">
//							Enter a list of samaple IDs to highlight
//							<OverlayTrigger trigger={['hover']} placement="right" overlay={tooltipModal}>
//
//							</OverlayTrigger>
//						</Modal.Title>
//					</Modal.Header>
//					<Modal.Body>
//						<Input style={{width: 550, height: 200}}
//							value={this.state.value}
//							type ="textarea"
//							placeholder={help}
//							onChange={this.onChange}/>
//					</Modal.Body>
//					<Modal.Footer>
//						<Button bsStyle="primary" onClick={this.submit} disabled={!this.state.value}>Submit</Button>
//						<Button onClick={this.close}>Cancel</Button>
//					</Modal.Footer>
//				</Modal>
//
//		);
//	}
//});

function findSubExpr(offsets, i) {
	var j = 0;
	while (i > offsets[j]) {
		++j;
	}
	return j;
}
var tooltips = {
	filter: 'Filter samples',
	subgroup: 'Create sample subgroups from data on the screen',
	find: 'Highlight or zoom in on a selection of samples',
	clear: 'Clear current filter and show all samples in the study',
	input: 'Examples: "missense", "null","B:>2"',
	keep: 'Keep only the selected samples on the screen',
	remove: 'Remove the selected samples from the screen',
	makeSubgroup: 'Matched samples will be one subgroup and non-matched samples will be the other subgroup',
	highlight: 'Highlight selected samples',
	zoom: 'Zoom in to selected samples',
	history: 'Previous search text'
};

var placeholder = {
	true: 'Click on visual spreadsheet or type here to select samples',
	false: 'Type here to select samples'
};

var input = comp => {
	var {matches, mode, history, pickSamples} = comp.props,
		{value} = comp.state,
		hasHistory = history.length > 0,
		noshow = (mode !== "heatmap");

	return (<TooltipInput key='input'
			className={classNames(compStyles.inputContainer, pickSamples && compStyles.picking)}
			tooltip={tooltips.input}
			onKeyUp={comp.onCaret}
			onClick={comp.onCaret}
			onFocus={comp.onCaret}
			onBlur={comp.onHideCaret}
			inputRef={comp.setRef}
			spellCheck={false}
			type='text'
			value={value || ''}
			placeholder={placeholder[pickSamples]}
			onChange={comp.onChange}
			disabled={noshow}>
		<TooltipI tooltip={tooltips.history} onClick={hasHistory ? comp.onOpenHistory : null}
				className={classNames(compStyles.dropDownArrow,
					hasHistory && compStyles.hasHistory, 'material-icons')}>
				arrow_drop_down</TooltipI>

		<Menu theme={{static: compStyles.history, active: compStyles.historyActive}}
				onShow={comp.onShowHistory} onHide={comp.onHideHistory}
				position='static' active={comp.state.historyOpen}>
			{history.map((b, i) =>
					// Note that Menu onSelect is broken in dev because of component
					// identity comparisons, and MenuItem events are broken in prod
					// because of RT deferring callbacks with a setState call, thus
					// allowing currentTarget to be nulled, so our only option here
					// is to create closures on every render. Very annoying.
					<MenuItem className={compStyles.menuItem} onClick={() => comp.onHistory(b)}
						data-value={b} key={i} value={b} caption={b}/>)}
		</Menu>
		{`${matches} matching samples`}
	</TooltipInput>);
};

export class SampleSearch extends PureComponent {
	state = {value: this.props.value};

	componentDidMount() {
		var highlight = new Rx.Subject();
		this.highlight = highlight.next.bind(highlight);
		this.sub = highlight.startWith(undefined).distinctUntilChanged()
			.subscribe(hl => this.props.onHighlightSelect(hl));
	}

	componentWillUnmount() {
		this.sub.unsubscribe();
	}

	componentWillReceiveProps(newProps) {
		if (this.state.value === this.props.value) {
			this.setState({value: newProps.value});
		}
		// otherwise we have buffered changes to state, and
		// updating from props would revert the user input
		// and move the carat to the end.
	}

	componentDidUpdate() {
		var oldSearch = this.props.oldSearch;
		// oldSearch will update before value, so we have to check for both.
		if (oldSearch != null && this.props.value) {
			// We have to set focus for the selection to be visible. If this
			// is problematic, we'll need to implement our own selection highlight
			// until the element has focus.
			this.input.focus({preventScroll: true});
			this.input.setSelectionRange(oldSearch.length, this.props.value.length);
			this.input.scrollLeft = this.input.scrollWidth;
		}
	}

	onChange = (value) => {
		var {onChange} = this.props;
		this.setState({value});
		onChange(value);
	};

	onSamplesSubmit = (value) => {
		var {onChange} = this.props;
		this.setState({value});
		onChange(value);
	};

	setRef = ref => {
		this.input = ref && ref.inputNode;
	}

	onCaret = () => {
		var hl = this.input && this.props.offsets ?
			findSubExpr(this.props.offsets, this.input.selectionStart) :
			undefined;
		this.highlight(hl);
	}

	onHideCaret = () => {
		this.highlight(undefined);
	}

	onOpenHistory = () => {
		this.setState({historyOpen: !this.state.historyOpen});
	}

	onHistory = value => {
		this.setState({historyOpen: false});
		this.props.onHistory(value); // move to front of history
		this.props.onChange(value);
	}

	// These two are hacks to keep local state synced with the menu, e.g.
	// when the user closes it by clicking outside the menu.
	onShowHistory = () => {
		this.setState({historyOpen: true});
	}

	onHideHistory = () => {
		this.setState({historyOpen: false});
	}

	onRemove = () => {
		this.props.onHistory(this.state.value.trim());
		this.props.onFilter(true);
	}

	onKeep = () => {
		this.props.onHistory(this.state.value.trim());
		this.props.onFilter(false);
	}

	onCreateColumn = () => {
		// have to add to history before the columns reorder, to
		// prevent the history having wrong column ids.
		this.props.onHistory(this.state.value.trim());
		this.props.onCreateColumn();
	}

	onZoom = () => {
		this.props.onHistory(this.state.value.trim());
		this.props.onZoom();
	}

	render() {
		var {matches, sampleCount, mode, pickSamples, onPickSamples, onResetSampleFilter} = this.props,
			disableActions = !(matches > 0 && matches < sampleCount),
			noshow = (mode !== "heatmap");
		return (

				{input(this)}
				<TooltipI className={classNames('material-icons', pickSamples ? compStyles.dark : '')}
					onClick={onPickSamples} tooltip='Pick samples'>colorize</TooltipI>
				{noshow ? filter_list :
				<TooltipIconMenu tooltip='Filter actions' className={compStyles.filterMenu} icon='filter_list' iconRipple={false} position='topLeft'>
					<MenuItem disabled={disableActions} caption='Keep samples' onClick={this.onKeep}/>
					<MenuItem disabled={disableActions} caption='Remove samples' onClick={this.onRemove}/>
					<MenuItem caption='Clear Filter' onClick={onResetSampleFilter}/>
					<MenuItem disabled={disableActions} caption='Zoom' onClick={this.onZoom}/>
					<MenuItem disabled={disableActions} caption='New Column' onClick={this.onCreateColumn}/>
				</TooltipIconMenu>}

		);
	}
}
<|endoftext|>


<commit_before>/*jslint node: true */
"use strict";

var Stopwatch = require('../lib/Stopwatch');
var defaultPrecision = 15;
var assert = require('assert');
var should = require('should');

describe('stopwatch', function () {
    this.timeout(5000);

    describe('constructor', function () {
        it('w/no params', function () {
            var stopwatch = new Stopwatch();
            should.exist(stopwatch);
        });

        it('w/name as first param', function () {
            var stopwatch = new Stopwatch('metric-name');
            stopwatch.name().should.equal('metric-name');
        });

        it('w/autostart as first param', function () {
            var stopwatch = new Stopwatch(true);
            should.exist(stopwatch.name());
            stopwatch.name().should.not.equal(true);
        });

        it('w/both name and autostart', function () {
            var stopwatch = new Stopwatch('metric-name', true);
            stopwatch.name().should.equal('metric-name');
        });
    });

    it('start and read (100ms)', function (done) {
        var testtime = 100;

        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            var delta = stopwatch.read();
            verifyDelta(testtime, delta, defaultPrecision);
            done();
        }, testtime);
    });

    it('start and read (10ms)', function (done) {
        var testtime = 10;

        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            var delta = stopwatch.read();
            verifyDelta(testtime, delta, defaultPrecision);
            done();
        }, testtime);
    });

    it('autostart set to true automatically starts stopwatch', function (done) {
        var testtime = 10;

        var stopwatch = new Stopwatch(true);
        setTimeout(function () {
            var delta = stopwatch.read();
            verifyDelta(testtime, delta, defaultPrecision);
            done();
        }, testtime);
    });

    it('autostart set to false does NOT automatically start stopwatch', function (done) {
        var testtime = 10;

        var stopwatch = new Stopwatch(false);
        setTimeout(function () {
            stopwatch.read().should.be.NaN();
            done();
        }, testtime);
    });

    it('with two stopwatches, do independent reads of each', function (done) {
        var stopwatch1 = new Stopwatch();
        var stopwatch2 = new Stopwatch();

        stopwatch1.start();

        //start first stopwatch, then do a read .3s later
        var testtimeA = 300;
        setTimeout(function () {
            var delta1 = stopwatch1.read();
            verifyDelta(testtimeA, delta1, defaultPrecision);
        }, testtimeA);

        //start second stopwatch .1s second after the first, then do a read .5s later
        var testtimeB = 500;
        setTimeout(function () {
            stopwatch2.start();
            setTimeout(function () {
                var delta2 = stopwatch2.read();
                verifyDelta(testtimeB, delta2, defaultPrecision);
                done();
            }, testtimeB);
        }, 100);
    });

    it('start, stop, and read should return the time at stop', function (done) {
        var testtime = 100;

        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            stopwatch.stop();

            setTimeout(function () {
                var delta = stopwatch.read();
                verifyDelta(testtime, delta, defaultPrecision);
                done();
            }, 500);
        }, testtime);
    });

    it('performing read without start() or stop() returns NaN', function (done) {
        var stopwatch = new Stopwatch();
        assert.ok(isNaN(stopwatch.read()));
        done();
    });

    it('performing read without start() returns NaN', function (done) {
        var stopwatch = new Stopwatch();
        stopwatch.stop();
        assert.ok(isNaN(stopwatch.read()));
        done();
    });

    it('executing stop twice should return time at second stop', function (done) {
        //start(), wait .1s, stop(), wait .2s, stop(), wait .5s, read(), ensure delta = .3s
        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            stopwatch.stop();

            setTimeout(function () {
                verifyDelta(100 + 200, stopwatch.stop(), defaultPrecision);

                setTimeout(function () {
                    var delta = stopwatch.read();
                    verifyDelta(100 + 200, delta, defaultPrecision);
                    done();
                }, 500);
            }, 200);
        }, 100);
    });

    it('ensure that stop() returns time', function (done) {
        var testtime = 100;
        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            var delta = stopwatch.stop();
            verifyDelta(testtime, delta, defaultPrecision);
            done();
        }, testtime);
    });
});

//TODO:
//replace with: statman.TestHelper.assertCloseEnough(testtime, delta, defaultPrecision);
function verifyDelta(expected, actual, acceptedVariance) {
    var lowerThreshold = expected - acceptedVariance;
    var upperThreshold = expected + acceptedVariance;
    var message = "Expected " + expected + " ± " + acceptedVariance + ", was " + actual + ".";
    assert.ok((actual >= lowerThreshold) && (actual <= upperThreshold), message);
}<commit_msg>add support for name<commit_after>/*jslint node: true */
"use strict";

var Stopwatch = require('../lib/Stopwatch');
var defaultPrecision = 15;
var assert = require('assert');
var should = require('should');

describe('stopwatch', function () {
    this.timeout(5000);

    describe('constructor', function () {
        it('w/no params', function () {
            var stopwatch = new Stopwatch();
            should.exist(stopwatch);
        });

        it('w/name as first param', function () {
            var stopwatch = new Stopwatch('metric-name');
            stopwatch.name().should.equal('metric-name');
        });

        it('w/autostart as first param', function () {
            var stopwatch = new Stopwatch(true);
            should.exist(stopwatch.name());
            stopwatch.name().should.not.equal(true);
        });

        it('w/both name and autostart', function () {
            var stopwatch = new Stopwatch('metric-name', true);
            stopwatch.name().should.equal('metric-name');
        });

        it('autocreated names are not constant', function () {
            var stopwatch1 = new Stopwatch(true);
            var stopwatch2 = new Stopwatch(true);
            console.log('stopwatch1:',stopwatch1.name());
            console.log('stopwatch2:',stopwatch2.name());
            stopwatch1.name().should.not.equal(stopwatch2.name());
        });
    });

    it('start and read (100ms)', function (done) {
        var testtime = 100;

        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            var delta = stopwatch.read();
            verifyDelta(testtime, delta, defaultPrecision);
            done();
        }, testtime);
    });

    it('start and read (10ms)', function (done) {
        var testtime = 10;

        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            var delta = stopwatch.read();
            verifyDelta(testtime, delta, defaultPrecision);
            done();
        }, testtime);
    });

    it('autostart set to true automatically starts stopwatch', function (done) {
        var testtime = 10;

        var stopwatch = new Stopwatch(true);
        setTimeout(function () {
            var delta = stopwatch.read();
            verifyDelta(testtime, delta, defaultPrecision);
            done();
        }, testtime);
    });

    it('autostart set to false does NOT automatically start stopwatch', function (done) {
        var testtime = 10;

        var stopwatch = new Stopwatch(false);
        setTimeout(function () {
            stopwatch.read().should.be.NaN();
            done();
        }, testtime);
    });

    it('with two stopwatches, do independent reads of each', function (done) {
        var stopwatch1 = new Stopwatch();
        var stopwatch2 = new Stopwatch();

        stopwatch1.start();

        //start first stopwatch, then do a read .3s later
        var testtimeA = 300;
        setTimeout(function () {
            var delta1 = stopwatch1.read();
            verifyDelta(testtimeA, delta1, defaultPrecision);
        }, testtimeA);

        //start second stopwatch .1s second after the first, then do a read .5s later
        var testtimeB = 500;
        setTimeout(function () {
            stopwatch2.start();
            setTimeout(function () {
                var delta2 = stopwatch2.read();
                verifyDelta(testtimeB, delta2, defaultPrecision);
                done();
            }, testtimeB);
        }, 100);
    });

    it('start, stop, and read should return the time at stop', function (done) {
        var testtime = 100;

        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            stopwatch.stop();

            setTimeout(function () {
                var delta = stopwatch.read();
                verifyDelta(testtime, delta, defaultPrecision);
                done();
            }, 500);
        }, testtime);
    });

    it('performing read without start() or stop() returns NaN', function (done) {
        var stopwatch = new Stopwatch();
        assert.ok(isNaN(stopwatch.read()));
        done();
    });

    it('performing read without start() returns NaN', function (done) {
        var stopwatch = new Stopwatch();
        stopwatch.stop();
        assert.ok(isNaN(stopwatch.read()));
        done();
    });

    it('executing stop twice should return time at second stop', function (done) {
        //start(), wait .1s, stop(), wait .2s, stop(), wait .5s, read(), ensure delta = .3s
        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            stopwatch.stop();

            setTimeout(function () {
                verifyDelta(100 + 200, stopwatch.stop(), defaultPrecision);

                setTimeout(function () {
                    var delta = stopwatch.read();
                    verifyDelta(100 + 200, delta, defaultPrecision);
                    done();
                }, 500);
            }, 200);
        }, 100);
    });

    it('ensure that stop() returns time', function (done) {
        var testtime = 100;
        var stopwatch = new Stopwatch();
        stopwatch.start();
        setTimeout(function () {
            var delta = stopwatch.stop();
            verifyDelta(testtime, delta, defaultPrecision);
            done();
        }, testtime);
    });
});

//TODO:
//replace with: statman.TestHelper.assertCloseEnough(testtime, delta, defaultPrecision);
function verifyDelta(expected, actual, acceptedVariance) {
    var lowerThreshold = expected - acceptedVariance;
    var upperThreshold = expected + acceptedVariance;
    var message = "Expected " + expected + " ± " + acceptedVariance + ", was " + actual + ".";
    assert.ok((actual >= lowerThreshold) && (actual <= upperThreshold), message);
}<|endoftext|>


<commit_before>'use strict';

var $ = require('jquery');
var qs = require('querystring').parse(window.location.search.slice(1));
var store = require('store');

var core = {};
var url = 'http://54.204.149.4:3001/';
if (qs.local) url = 'http://127.0.0.1:3001/';

function request(error, callback) {
    $.ajax({
        crossDomain: true,
        url: url + 'error/' + error,
        type: 'post',
        data: JSON.stringify({user: store.get('username')})
    })
    .error(jqError)
    .done(callback);
}

core.item = function(error, callback) {
    // eventually remove the need for specifying error
    // can get by on callback only
    request(error, function(data) {
        data = JSON.parse(data);
        window.current.item = data.value;
        window.current.item._id = data.key;
        return callback();
    });
};

core.mark = function(status, callback) {
    // mark it as done/inadequate/needing review, mark it as something
    // do we do those mappings here or on the server?
    // literal strings?
    // leave the definitions fluid on purpose
    // let the loader pick what status to pay attention to and ignore?

    // not doing anything with status yet, we'll want to sort by it eventually
    // thinking about hstore for "state" eventually, need to think about it a bit

    $.ajax({
        crossDomain: true,
        url: url + 'fixed/' + qs.error,
        type: 'post',
        data: JSON.stringify({
            user: store.get('username'),
            state: window.current.item
        })
    })
    .error(jqError)
    .done(callback);

    // state: current
    // not liking that at all
};

core.error = function(message) {
    $('#error-message span').text(message).show();
    $('#error-message').slideDown();
    setTimeout(function() {
        $('#error-message span').fadeOut(function() {
            $('#error-message').slideUp();
        });
    }, 5000);
};

core.upload = function(formData, callback) {
    $.ajax({
        crossDomain: true,
        url: url + 'csv',
        type: 'post',
        data: formData,
        processData: false,
        contentType: false
    })
    .error(callback)
    .done(function() {
        callback();
    });
};

function jqError(jqXHR, textStatus) {
    core.error(textStatus === 'timeout' ?
        'Request timed out.' :
        jqXHR.responseText
    );
}

module.exports = core;
<commit_msg>Set content type<commit_after>'use strict';

var $ = require('jquery');
var qs = require('querystring').parse(window.location.search.slice(1));
var store = require('store');

var core = {};
var url = 'http://54.204.149.4:3001/';
url = 'http://localhost:8000/';
// url = 'http://54.237.126.85:8000/';
if (qs.local) url = 'http://127.0.0.1:3001/';

function request(error, callback) {
    $.ajax({
        crossDomain: true,
        url: url + 'error/' + error,
        type: 'POST',
        data: JSON.stringify({user: store.get('username')})
    })
    .error(jqError)
    .done(callback);
}

core.item = function(error, callback) {
    // eventually remove the need for specifying error
    // can get by on callback only
    request(error, function(data) {
        data = JSON.parse(data);
        window.current.item = data.value;
        window.current.item._id = data.key;
        return callback();
    });
};

core.mark = function(status, callback) {
    // mark it as done/inadequate/needing review, mark it as something
    // do we do those mappings here or on the server?
    // literal strings?
    // leave the definitions fluid on purpose
    // let the loader pick what status to pay attention to and ignore?

    // not doing anything with status yet, we'll want to sort by it eventually
    // thinking about hstore for "state" eventually, need to think about it a bit

    $.ajax({
        crossDomain: true,
        url: url + 'fixed/' + qs.error,
        type: 'POST',
        data: JSON.stringify({
            user: store.get('username'),
            state: window.current.item
        }),
        contentType: 'text/plain'
    })
    .error(jqError)
    .done(callback);

    // state: current
    // not liking that at all
};

core.error = function(message) {
    $('#error-message span').text(message).show();
    $('#error-message').slideDown();
    setTimeout(function() {
        $('#error-message span').fadeOut(function() {
            $('#error-message').slideUp();
        });
    }, 5000);
};

core.upload = function(formData, callback) {
    $.ajax({
        crossDomain: true,
        url: url + 'csv',
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false
    })
    .error(callback)
    .done(function() {
        callback();
    });
};

function jqError(jqXHR, textStatus) {
    core.error(textStatus === 'timeout' ?
        'Request timed out.' :
        jqXHR.responseText
    );
}

module.exports = core;
<|endoftext|>


<commit_before>var dateFormat = require('dateformat');

Entry.create = function(gEntry) {
    return new Entry(gEntry);
}

function Entry(gEntry) {
    this._id = gEntry.id;
    this._title = gEntry.title;
    this._url = getUrlFor(gEntry);
    this._tstamp = gEntry.updated;
    this._summary = gEntry.summary ? gEntry.summary.content : "";
    this._isFresh = getIsFresh(gEntry.categories);
}

function getUrlFor(gEntry) {
    if (gEntry.items && gEntry.items.length > 0) {
        var alternate = gEntry.items[0].alternate;
        if (alternate) return alternate[0].href;
    }
    return "";
}

function getIsFresh(categories) {
    if (categories === undefined) return false;
    for (var i = 0; i < categories.length; i++) {
        if (categories[i].match(/fresh$/)) { return true; }
    }
    return false;
}

Entry.get = function(id, cb) {
    return Entry.GReader().getEntry(id, cb);
}

Entry.GReader = function() {
    return require('../services/class.greader.js');
}

Entry.prototype.id = function(id) {
    if (id === undefined) {
        return this._id;
    }
    this._id = id;
}

Entry.prototype.title = function(title) {
    if (title === undefined) {
        return this._title;
    }
    this._title = title;
}

Entry.prototype.shortTitle = function() {
    return this._title && this._title.length > 34 ?
        this._title.substr(0, 31) + "..." : this._title;
};

Entry.prototype.url = function(url) {
    if (url === undefined) {
        return this._url;
    }
    this._url = url;
}

Entry.prototype.tstamp = function(tstamp) {
    if (tstamp === undefined) {
        return this._tstamp;
    }
    this._tstamp = tstamp;
}

Entry.prototype.summary = function(summary) {
    if (summary === undefined) {
        return this._summary;
    }
    this._summary = summary;
}

Entry.prototype.isFresh = function(isFresh) {
    if (isFresh === undefined) {
        return this._isFresh;
    }
    this._isFresh = isFresh;
}

Entry.prototype.date = function() {
    return dateFormat(this._tstamp * 1000, "dd mmm, yyyy");
};

module.exports = Entry;
<commit_msg>Refactored code for knowing if an item is fresh, but the logic doesn't work<commit_after>var dateFormat = require('dateformat');

Entry.create = function(gEntry) {
    return new Entry(gEntry);
}

function Entry(gEntry) {
    this._id = gEntry.id;
    this._title = gEntry.title;
    this._url = getUrlFor(gEntry);
    this._tstamp = gEntry.updated;
    this._summary = gEntry.summary ? gEntry.summary.content : "";
    this._isFresh = getIsFresh(gEntry);
}

function getUrlFor(gEntry) {
    if (gEntry.items && gEntry.items.length > 0) {
        var alternate = gEntry.items[0].alternate;
        if (alternate) return alternate[0].href;
    }
    return "";
}

/**
 * FIXME:
 * Supposedly when an item is fresh it has a tag 'fresh' on its categories, but it's not correct
 * Still trying to figure out how to know if an item is fresh so temporarily leave it 'fresh' always
 */
function getIsFresh(gEntry) {
    /*
    if (gEntry.categories) {
        gEntry.categories.forEach(function(category) {
            if (category.match(/fresh$/)) return true;
        });
    }
    return false;
    */
    return true;
}

Entry.get = function(id, cb) {
    return Entry.GReader().getEntry(id, cb);
}

Entry.GReader = function() {
    return require('../services/class.greader.js');
}

Entry.prototype.id = function(id) {
    if (id === undefined) {
        return this._id;
    }
    this._id = id;
}

Entry.prototype.title = function(title) {
    if (title === undefined) {
        return this._title;
    }
    this._title = title;
}

Entry.prototype.shortTitle = function() {
    return this._title && this._title.length > 34 ?
        this._title.substr(0, 31) + "..." : this._title;
};

Entry.prototype.url = function(url) {
    if (url === undefined) {
        return this._url;
    }
    this._url = url;
}

Entry.prototype.tstamp = function(tstamp) {
    if (tstamp === undefined) {
        return this._tstamp;
    }
    this._tstamp = tstamp;
}

Entry.prototype.summary = function(summary) {
    if (summary === undefined) {
        return this._summary;
    }
    this._summary = summary;
}

Entry.prototype.isFresh = function(isFresh) {
    if (isFresh === undefined) {
        return this._isFresh;
    }
    this._isFresh = isFresh;
}

Entry.prototype.date = function() {
    return dateFormat(this._tstamp * 1000, "dd mmm, yyyy");
};

module.exports = Entry;
<|endoftext|>


<commit_before>// @flow

import React from 'react'
import { StyleSheet } from 'react-native'
import { TabViewAnimated, TabBarTop } from 'react-native-tab-view'
import * as c from '../colors'
import type { TabbedViewPropsType } from './types'
import { TabbedViewPropTypes } from './types'

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  tabbar: {
    height: 48,
    backgroundColor: c.mandarin,
  },
  page: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  indicator: {
    backgroundColor: '#ffeb3b',
  },
  label: {
    color: '#fff',
    fontWeight: '400',
  },
})

export default class TabbedView extends React.Component {
  static propTypes = TabbedViewPropTypes

  state = {
    index: 0,
  }

  props: TabbedViewPropsType

  _handleChangeTab = index => {
    this.setState({
      index,
    })
  }

  _renderHeader = props => {
    return (
      <TabBarTop
        {...props}
        scrollEnabled={true}
        indicatorStyle={styles.indicator}
        style={styles.tabbar}
        labelStyle={styles.label}
      />
    )
  }

  _renderScene = ({ route }) => {
    if (!route.component) {
      return null
    }

    return (
      <route.component
        {...this.props.childProps}
        {...(route.props || {})}
        navigator={this.props.navigator}
        route={this.props.route}
      />
    )
  }

  render() {
    let routes = {routes: this.props.tabs.map(tab => ({...tab, key: tab.id}))}

    return (
      <TabViewAnimated
        style={styles.container}
        navigationState={{...this.state, ...routes}}
        renderScene={this._renderScene}
        renderHeader={this._renderHeader}
        onRequestChangeTab={this._handleChangeTab}
      />
    )
  }
}
<commit_msg>condense the font for the tab titles on android<commit_after>// @flow

import React from 'react'
import { StyleSheet, Platform } from 'react-native'
import { TabViewAnimated, TabBarTop } from 'react-native-tab-view'
import * as c from '../colors'
import type { TabbedViewPropsType } from './types'
import { TabbedViewPropTypes } from './types'

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  tabbar: {
    height: 48,
    backgroundColor: c.mandarin,
  },
  page: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  indicator: {
    backgroundColor: '#ffeb3b',
  },
  label: {
    color: '#fff',
    fontFamily: Platform.OS === 'ios' ? 'System' : 'sans-serif-condensed',
    fontWeight: '400',
  },
})

export default class TabbedView extends React.Component {
  static propTypes = TabbedViewPropTypes

  state = {
    index: 0,
  }

  props: TabbedViewPropsType

  _handleChangeTab = index => {
    this.setState({
      index,
    })
  }

  _renderHeader = props => {
    return (
      <TabBarTop
        {...props}
        scrollEnabled={true}
        indicatorStyle={styles.indicator}
        style={styles.tabbar}
        labelStyle={styles.label}
      />
    )
  }

  _renderScene = ({ route }) => {
    if (!route.component) {
      return null
    }

    return (
      <route.component
        {...this.props.childProps}
        {...(route.props || {})}
        navigator={this.props.navigator}
        route={this.props.route}
      />
    )
  }

  render() {
    let routes = {routes: this.props.tabs.map(tab => ({...tab, key: tab.id}))}

    return (
      <TabViewAnimated
        style={styles.container}
        navigationState={{...this.state, ...routes}}
        renderScene={this._renderScene}
        renderHeader={this._renderHeader}
        onRequestChangeTab={this._handleChangeTab}
      />
    )
  }
}
<|endoftext|>


<commit_before>'use strict';

const EventEmitter = require('events').EventEmitter;
const execSync = require('child_process').execSync;
const os = require('os');

function convertPowershellDate(date) {
  const parts = date.toString().split(' ');
  return `${parts[0]} ${parts[1]} ${parts[2]} ${parts[3]} ${parts[4]}`;
}

function processLogEvent(event) {
  let createdAtMilis = event.TimeCreated.replace(/\//g, '').replace('Date(', '').replace(')', '');
  createdAtMilis = parseInt(createdAtMilis, 10);
  return {
    id: event.Id,
    providerName: event.ProviderName,
    providerId: event.ProviderId,
    logName: event.LogName,
    processId: event.ProcessId,
    threadId: event.ThreadId,
    machineName: event.MachineName,
    timeCreated: new Date(createdAtMilis),
    levelDisplayName: event.LevelDisplayName,
    message: event.Message
  };
}

function parseLogData(data) {
  let events;
  try {
    events = JSON.parse(data);
  } catch (e) {
    throw e;
  }

  return Array.isArray(events) ?
    events.map(event => processLogEvent(event)) :
    [processLogEvent(events)];
}

class WinEventEmitter extends EventEmitter {
  constructor (options) {
    super();

    if (!/win/gi.test(os.platform())) {
      throw new Error('Only Windows is supported');
    }

    const defaultOptions = {
      providers: ['Microsoft-Windows-DNS-Client'],
      maxEvents: 100,
      frequency: 10000
    };
    this.options = Object.assign({}, defaultOptions, options);
    this.options.startTime || (this.options.startTime = new Date());
    this.options.endTime  || (this.options.endTime = new Date(Date.now() + this.options.frequency));
    this._id = -1;
  }

  start () {
    this._id = setTimeout(() => {
      const providers = `'${this.options.providers.join('\', \'')}'`;
      let startTime = convertPowershellDate(this.options.startTime), endTime = convertPowershellDate(this.options.endTime);

      let creds = '';
      let credsOpt = '';
      if (this.options.userName && this.options.password) {
        creds =  `$User = '${this.options.userName}'; $Pass = ConvertTo-SecureString -String '${this.options.password}' -AsPlainText -Force; $Credential = New-Object –TypeName 'System.Management.Automation.PSCredential' –ArgumentList $User, $Pass; `;
        credsOpt = ' -Credentials $Credential';
      }

      let computerOpt = '';
      if (this.options.computerName) {
        computerOpt = ` -ComputerName ${this.options.computerName}`;
      }

      let powershellCmd = `powershell "${creds}Get-WinEvent -FilterHashTable @{ProviderName=${providers}; StartTime='${startTime}'; EndTime='${endTime}';} -MaxEvents ${this.options.maxEvents}${credsOpt}${computerOpt} | ConvertTo-Json`;
      this.emit('powerShellCmd', powerSHellCmd)
      let data;
      try {
        data = execSync(powershellCmd, {stdio: [1]}).toString();
        this.emit('raw.data', data)
      } catch (e) {
        // Ignore missing events
        if (!e.message.indexOf('NoMatchingEventsFound,Microsoft.PowerShell.Commands.GetWinEventCommand')) {
          this.emit('error', e);
        }
      }

      if (this._stop) { return; }
      var parsedData = null
      if (data) {
        try {
          parsedData = parseLogData(data)
          this.emit('data', parsedData)
        } catch (err) {
          this.emit('error', err)
        }
      }

      // Ensure no time gaps
      var now = Date.now()
      this.options.startTime = new Date(this.options.endTime || now)
      this.options.endTime = new Date(now + this.options.frequency)

      this.start();
    }, this.options.frequency);
  }

  stop () {
    clearTimeout(this._id);
    this._stop = true;
    this.emit('end');
  }
}

module.exports = WinEventEmitter;
<commit_msg>add '*' alias as 'default' provider, fix #1<commit_after>'use strict';

const EventEmitter = require('events').EventEmitter;
const execSync = require('child_process').execSync;
const os = require('os');

function convertPowershellDate(date) {
  const parts = date.toString().split(' ');
  return `${parts[0]} ${parts[1]} ${parts[2]} ${parts[3]} ${parts[4]}`;
}

function processLogEvent(event) {
  let createdAtMilis = event.TimeCreated.replace(/\//g, '').replace('Date(', '').replace(')', '');
  createdAtMilis = parseInt(createdAtMilis, 10);
  return {
    id: event.Id,
    providerName: event.ProviderName,
    providerId: event.ProviderId,
    logName: event.LogName,
    processId: event.ProcessId,
    threadId: event.ThreadId,
    machineName: event.MachineName,
    timeCreated: new Date(createdAtMilis),
    levelDisplayName: event.LevelDisplayName,
    message: event.Message
  };
}

function parseLogData(data) {
  let events;
  try {
    events = JSON.parse(data);
  } catch (e) {
    throw e;
  }

  return Array.isArray(events) ?
    events.map(event => processLogEvent(event)) :
    [processLogEvent(events)];
}

class WinEventEmitter extends EventEmitter {
  constructor (options) {
    super();

    if (!/win/gi.test(os.platform())) {
      throw new Error('Only Windows is supported');
    }

    const defaultOptions = {
      providers: ['*'],
      maxEvents: 100,
      frequency: 10000
    };
    this.options = Object.assign({}, defaultOptions, options);
    this.options.startTime || (this.options.startTime = new Date());
    this.options.endTime  || (this.options.endTime = new Date(Date.now() + this.options.frequency));
    this._id = -1;
  }

  start () {
    this._id = setTimeout(() => {
      const providers = null
      const getAllLogs = (this.options.providers && this.options.providers.length > 0 && this.options.provider[0]=='*')
      if (!getAllLogs) {
        providers = `'${this.options.providers.join('\', \'')}'`;
      }
      let startTime = convertPowershellDate(this.options.startTime), endTime = convertPowershellDate(this.options.endTime);

      let creds = '';
      let credsOpt = '';
      if (this.options.userName && this.options.password) {
        creds =  `$User = '${this.options.userName}'; $Pass = ConvertTo-SecureString -String '${this.options.password}' -AsPlainText -Force; $Credential = New-Object –TypeName 'System.Management.Automation.PSCredential' –ArgumentList $User, $Pass; `;
        credsOpt = ' -Credentials $Credential';
      }

      let computerOpt = '';
      if (this.options.computerName) {
        computerOpt = ` -ComputerName ${this.options.computerName}`;
      }

      let powershellCmd = `powershell "${creds}Get-WinEvent -FilterHashTable @{ProviderName=${providers}; StartTime='${startTime}'; EndTime='${endTime}';} -MaxEvents ${this.options.maxEvents}${credsOpt}${computerOpt} | ConvertTo-Json`;
      if (getAllLogs) {
        powerSHellCmd = `powershell "${creds}Get-WinEvent ---ListLog  *; StartTime='${startTime}'; EndTime='${endTime}';} -MaxEvents ${this.options.maxEvents}${credsOpt}${computerOpt} | ConvertTo-Json`;
      }
      this.emit('powerShellCmd', powerSHellCmd)
      let data;
      try {
        data = execSync(powershellCmd, {stdio: [1]}).toString();
        this.emit('raw.data', data)
      } catch (e) {
        // Ignore missing events
        if (!e.message.indexOf('NoMatchingEventsFound,Microsoft.PowerShell.Commands.GetWinEventCommand')) {
          this.emit('error', e);
        }
      }

      if (this._stop) { return; }
      var parsedData = null
      if (data) {
        try {
          parsedData = parseLogData(data)
          this.emit('data', parsedData)
        } catch (err) {
          this.emit('error', err)
        }
      }

      // Ensure no time gaps
      var now = Date.now()
      this.options.startTime = new Date(this.options.endTime || now)
      this.options.endTime = new Date(now + this.options.frequency)

      this.start();
    }, this.options.frequency);
  }

  stop () {
    clearTimeout(this._id);
    this._stop = true;
    this.emit('end');
  }
}

module.exports = WinEventEmitter;
<|endoftext|>


<commit_before>var autosize = require("autosize");
var constants = require("../util/constants");
var CurrentUserModel = require("../stores/currentUser");
var View = require("handlebones").View;
var template = require("../tmpl/comment-form");
var CommentModel = require("../models/comment");
var CommentView = require("../views/commentView");
var display = require("../util/display");
var eb = require("../eventBus");
var Handlebones = require("handlebones");
var PolisFacebookUtils = require('../util/facebookButton');
var ProfilePicView = require('../views/profilePicView');
var serialize = require("../util/serialize");
var Strings = require("../strings");
var Utils = require("../util/utils");

var CHARACTER_LIMIT = constants.CHARACTER_LIMIT;

// var CommentsByMeView = Handlebones.CollectionView.extend({
//   modelView: CommentView
// });

function reject() {
  return $.Deferred().reject();
}
function resolve() {
  return $.Deferred().resolve();
}

module.exports = Handlebones.ModelView.extend({
  name: "comment-form",
  template: template,

  // needed to prevent double submissions, which are annoying because they trigger a duplicate alert
  buttonActive: true,

  context: function() {
    var ctx = Handlebones.ModelView.prototype.context.apply(this, arguments);
    ctx = _.extend(ctx, this, this.model&&this.model.attributes);
    ctx.is_active = this.parent.model.get("is_active");
    ctx.shouldAutofocusOnTextarea = this.shouldAutofocusOnTextarea || Utils.shouldFocusOnTextareaWhenWritePaneShown();
    ctx.hasTwitter = userObject.hasTwitter;
    ctx.hasFacebook = userObject.hasFacebook;
    ctx.s = Strings;
    ctx.desktop = !display.xs();

    var btnBg = preload.conversation.style_btn;
    if (btnBg) {
      ctx.customBtnStyles = "background-color: " + btnBg + ";";
    }
    return ctx;
  },
  hideMessage: function(id) {
    this.$(id).hide();
  },

  showMessage: function(id) {
    // since there are now other warnings/tips showing, hide the default one
    this.$(".low_priority_tip").hide();
    this.$(id).show();
  },
  updateOneIdeaPerCommentMessage: function(formText) {
    // TODO I18N
    // Tests to see if there is non-punctuation that follows the end of a sentence.
    if ((formText||"").match(/[\?\.\!].*[a-zA-Z0-9]+/)) {
      this.showMessage("#one_idea_per_comment_message");
    } else {
      this.hideMessage("#one_idea_per_comment_message");
      this.maybeShowBasicTip();
    }
  },
  updateCommentNotQuestionAlert: function(formText) {
    if (formText.indexOf("?") >= 0) {
      this.showMessage("#commentNotQuestionAlert");
    } else {
      this.hideMessage("#commentNotQuestionAlert");
      this.maybeShowBasicTip();
    }
  },
  maybeShowBasicTip: function() {
    // if there are no other warnings/tips showing, show the default one
    if (this.$(".protip:visible").length === 0) {
      this.chooseBasicTip();
    }
  },
  chooseBasicTip: function() {
    var form =  $("#comment_form_textarea");
    var formText = form.val();
    if (formText.length) {
      this.showMessage("#not_a_reply_message");
      this.hideMessage("#write_your_perspective_message");
    } else {
      this.showMessage("#write_your_perspective_message");
      this.hideMessage("#not_a_reply_message");
    }
  },
  textChange: function() {
    var form =  $(arguments[0].target);
    var formText = form.val();
    var len = formText.length;
    var remaining = CHARACTER_LIMIT - len;
    var txt;
    if (remaining < 0) {
      // txt = "- " + remaining;
      txt = remaining;
      this.$("#commentCharCount").css("color", "red");
      this.$("#comment_button").attr("disabled", "disabled");
      this.$("#comment_button").css("opacity", 0.3);
      this.showMessage("#commentTooLongAlert");
      this.buttonActive = false;
    } else if (remaining > 0) {
      txt = /*"+ " +*/ remaining;
      this.$("#commentCharCount").css("color", "gray");
      this.$("#comment_button").attr("disabled", null);
      this.$("#comment_button").css("opacity", 1);
      this.hideMessage("#commentTooLongAlert");
      this.maybeShowBasicTip();
      this.buttonActive = true;
    } else {
      txt = remaining;
      this.$("#commentCharCount").css("color", "gray");
      this.$("#comment_button").attr("disabled", null);
      this.$("#comment_button").css("opacity", 1);
      this.hideMessage("#commentTooLongAlert");
      this.maybeShowBasicTip();
      this.buttonActive = true;
    }
    this.$("#commentCharCount").text(txt);
    this.chooseBasicTip(formText);
    this.updateOneIdeaPerCommentMessage(formText);
    this.updateCommentNotQuestionAlert(formText);
    eb.trigger(eb.interacted);
  },
  showFormControls: function() {
    // this.$(".alert").hide();
    this.$(".comment_form_control_hideable").show();
  },
  hideFormControls: function() {
    // this.$(".comment_form_control_hideable").hide();
    // this.$("#commentCharCount").text("");
  },
  reloadPagePreservingCommentText: function() {
    var wipCommentFormText = $("#comment_form_textarea").val();
    var params = {};
    if (wipCommentFormText.length) {
      params.wipCommentFormText = wipCommentFormText;
    }
    eb.trigger(eb.reloadWithMoreParams, params);
  },
  events: {
    "focus #comment_form_textarea": function(e) { // maybe on keyup ?
      this.showFormControls();
    },
    "blur #comment_form_textarea": function(e) {
      var txt = this.$("#comment_form_textarea").val();
      if (!txt || !txt.length) {
        this.hideFormControls();
      }
    },
    "change #comment_form_textarea": "textChange",
    "keyup #comment_form_textarea": "textChange",
    "paste #comment_form_textarea": "textChange",
    "click #facebookButtonCommentForm" : "facebookClicked",
    "click #twitterButtonCommentForm" : "twitterClicked",
    "click #comment_button": "onSubmitClicked",
  },
  onSubmitClicked: function(e) {
    e.preventDefault();
    this.submitComment();
  },
  submitComment: function(e){
    var that = this;

    function doSubmitComment() {
      if (that.buttonActive) {
        that.buttonActive = false;
        serialize(that, function(attrs){
          that.participantCommented(attrs).then(function() {
            that.$("#comment_form_textarea").val("");
            that.hideFormControls();
            that.showMessage("#comment_sent_message");

          }).always(function() {
            that.buttonActive = true;
          });
        });
      }
    }

    var hasSocial = window.userObject.hasFacebook || window.userObject.hasTwitter;
    if (hasSocial) {
      doSubmitComment();
    } else {
      this.showSocialAuthChoices();
    }
  },
  onAuthSuccess: function() {
    this.reloadPagePreservingCommentText();
    // $("#socialButtonsCommentForm").hide();
    // $("#comment_form_controls").show();
  },
  facebookClicked: function(e) {
    e.preventDefault();
    var that = this;
    PolisFacebookUtils.connect().then(function() {
      // wait a bit for new cookies to be ready, or something, then submit comment.
      setTimeout(function() {
        that.onAuthSuccess();
        // CurrentUserModel.update();
      }, 100);
    }, function(err) {
      // alert("facebook error");
    });
  },
  twitterClicked: function(e) {
    var that = this;
    e.preventDefault();

    eb.on(eb.twitterConnected, function() {
      // wait a bit for new cookies to be ready, or something, then submit comment.
      setTimeout(function() {
        that.onAuthSuccess();
        // CurrentUserModel.update();
      }, 100);
    });
    // open a new window where the twitter auth screen will show.
    // that window will redirect back to a simple page that calls window.opener.twitterStatus("ok")
    var params = 'location=0,status=0,width=800,height=400';
    window.open(document.location.origin + "/api/v3/twitterBtn?dest=/twitterAuthReturn", 'twitterWindow', params);
  },
  showSocialAuthChoices: function() {
    $("#comment_form_controls").hide();
    $("#socialButtonsCommentForm").show();
    $("#socialButtonsUnderReadReact").hide();
  },
  participantCommented: function(attrs) {
    var that = this; //that = the view
    attrs.pid = "mypid";
    attrs.conversation_id = this.conversation_id;
    attrs.vote = constants.REACTIONS.AGREE; // participants' comments are automatically agreed to. Needed for now since math assumes every comment has at least one vote.

    if (/^\s*$/.exec(attrs.txt)) {
      alert("Comment is empty");
      return reject();
    }
    if (attrs.txt.length > 997) {
      alert("Comment is too long");
      return reject();
    }

    // replace newlines with whitespace
    attrs.txt = attrs.txt.replace(/\n/g, " ");

    if (Utils.isDemoMode()) {
      that.showMessage("#comment_sent_message");
      that.trigger("commentSubmitted");
      return resolve();
    }

    var promise = that.serverClient.submitComment(attrs);
    if (!promise) {
      return reject();
    } else {
      promise.then(function() {
        that.trigger("commentSubmitted"); // view.trigger
        // $("#comment_form_textarea").hide();
        // $("#commentSentAlert").fadeIn(300);
        // setTimeout(function() {
        //   $("#commentSentAlert").fadeOut(500, function() {
        //     $("#comment_form_textarea").fadeIn(400);
        //   });
        // }, 1500);
      }, function(args) {
        if (!args || !args.length || !args[0].length) {
          alert("failed to send");
          return;
        }
        var err = args[0][0];
        if (err.status === 409) {

          // that.model.set({
          //   error: "Duplicate!",
          //   errorExtra: "That comment already exists.",
          // });
          alert("Duplicate! That comment already exists.");
        } else if (err.responseText === "polis_err_conversation_is_closed"){

          // that.model.set({
          //   error: "This conversation is closed.",
          //   errorExtra: "No further commenting is allowed.",
          // });
          alert("This conversation is closed. No further commenting is allowed.");
        } else {

          // that.model.set({
          //   error: "Error sending comment.",
          //   errorExtra: "Please try again later.",
          // });
          alert("Error sending comment, please try again later.");
        }
      });
      return promise;
    }
  },
  initialize: function(options) {
    Handlebones.ModelView.prototype.initialize.apply(this, arguments);
    this.model = options.model;
    this.conversation_id = options.conversation_id;
    this.collection = options.collection;
    // this.commentsByMeView = this.addChild(new CommentsByMeView({
    //   collection: options.collection
    // }));
    this.serverClient = options.serverClient;

    this.profilePicView = this.addChild(new ProfilePicView({
      model: CurrentUserModel,
    }));

    if (options.wipCommentFormText) {
      this.shouldAutofocusOnTextarea = true;
    }

    var that = this;
    this.listenTo(this, "render", function(){
      setTimeout(function() {
        if (!_.isUndefined(options.wipCommentFormText)) {
          $("#comment_form_textarea").val(options.wipCommentFormText);
          eb.trigger(eb.doneUsingWipCommentFormText);
          that.submitComment();
        }
        autosize($("#comment_form_textarea"));
      },100);
    });

  },
});
<commit_msg>fix comment post error handling<commit_after>var autosize = require("autosize");
var constants = require("../util/constants");
var CurrentUserModel = require("../stores/currentUser");
var View = require("handlebones").View;
var template = require("../tmpl/comment-form");
var CommentModel = require("../models/comment");
var CommentView = require("../views/commentView");
var display = require("../util/display");
var eb = require("../eventBus");
var Handlebones = require("handlebones");
var PolisFacebookUtils = require('../util/facebookButton');
var ProfilePicView = require('../views/profilePicView');
var serialize = require("../util/serialize");
var Strings = require("../strings");
var Utils = require("../util/utils");

var CHARACTER_LIMIT = constants.CHARACTER_LIMIT;

// var CommentsByMeView = Handlebones.CollectionView.extend({
//   modelView: CommentView
// });

function reject() {
  return $.Deferred().reject();
}
function resolve() {
  return $.Deferred().resolve();
}

module.exports = Handlebones.ModelView.extend({
  name: "comment-form",
  template: template,

  // needed to prevent double submissions, which are annoying because they trigger a duplicate alert
  buttonActive: true,

  context: function() {
    var ctx = Handlebones.ModelView.prototype.context.apply(this, arguments);
    ctx = _.extend(ctx, this, this.model&&this.model.attributes);
    ctx.is_active = this.parent.model.get("is_active");
    ctx.shouldAutofocusOnTextarea = this.shouldAutofocusOnTextarea || Utils.shouldFocusOnTextareaWhenWritePaneShown();
    ctx.hasTwitter = userObject.hasTwitter;
    ctx.hasFacebook = userObject.hasFacebook;
    ctx.s = Strings;
    ctx.desktop = !display.xs();

    var btnBg = preload.conversation.style_btn;
    if (btnBg) {
      ctx.customBtnStyles = "background-color: " + btnBg + ";";
    }
    return ctx;
  },
  hideMessage: function(id) {
    this.$(id).hide();
  },

  showMessage: function(id) {
    // since there are now other warnings/tips showing, hide the default one
    this.$(".low_priority_tip").hide();
    this.$(id).show();
  },
  updateOneIdeaPerCommentMessage: function(formText) {
    // TODO I18N
    // Tests to see if there is non-punctuation that follows the end of a sentence.
    if ((formText||"").match(/[\?\.\!].*[a-zA-Z0-9]+/)) {
      this.showMessage("#one_idea_per_comment_message");
    } else {
      this.hideMessage("#one_idea_per_comment_message");
      this.maybeShowBasicTip();
    }
  },
  updateCommentNotQuestionAlert: function(formText) {
    if (formText.indexOf("?") >= 0) {
      this.showMessage("#commentNotQuestionAlert");
    } else {
      this.hideMessage("#commentNotQuestionAlert");
      this.maybeShowBasicTip();
    }
  },
  maybeShowBasicTip: function() {
    // if there are no other warnings/tips showing, show the default one
    if (this.$(".protip:visible").length === 0) {
      this.chooseBasicTip();
    }
  },
  chooseBasicTip: function() {
    var form =  $("#comment_form_textarea");
    var formText = form.val();
    if (formText.length) {
      this.showMessage("#not_a_reply_message");
      this.hideMessage("#write_your_perspective_message");
    } else {
      this.showMessage("#write_your_perspective_message");
      this.hideMessage("#not_a_reply_message");
    }
  },
  textChange: function() {
    var form =  $(arguments[0].target);
    var formText = form.val();
    var len = formText.length;
    var remaining = CHARACTER_LIMIT - len;
    var txt;
    if (remaining < 0) {
      // txt = "- " + remaining;
      txt = remaining;
      this.$("#commentCharCount").css("color", "red");
      this.$("#comment_button").attr("disabled", "disabled");
      this.$("#comment_button").css("opacity", 0.3);
      this.showMessage("#commentTooLongAlert");
      this.buttonActive = false;
    } else if (remaining > 0) {
      txt = /*"+ " +*/ remaining;
      this.$("#commentCharCount").css("color", "gray");
      this.$("#comment_button").attr("disabled", null);
      this.$("#comment_button").css("opacity", 1);
      this.hideMessage("#commentTooLongAlert");
      this.maybeShowBasicTip();
      this.buttonActive = true;
    } else {
      txt = remaining;
      this.$("#commentCharCount").css("color", "gray");
      this.$("#comment_button").attr("disabled", null);
      this.$("#comment_button").css("opacity", 1);
      this.hideMessage("#commentTooLongAlert");
      this.maybeShowBasicTip();
      this.buttonActive = true;
    }
    this.$("#commentCharCount").text(txt);
    this.chooseBasicTip(formText);
    this.updateOneIdeaPerCommentMessage(formText);
    this.updateCommentNotQuestionAlert(formText);
    eb.trigger(eb.interacted);
  },
  showFormControls: function() {
    // this.$(".alert").hide();
    this.$(".comment_form_control_hideable").show();
  },
  hideFormControls: function() {
    // this.$(".comment_form_control_hideable").hide();
    // this.$("#commentCharCount").text("");
  },
  reloadPagePreservingCommentText: function() {
    var wipCommentFormText = $("#comment_form_textarea").val();
    var params = {};
    if (wipCommentFormText.length) {
      params.wipCommentFormText = wipCommentFormText;
    }
    eb.trigger(eb.reloadWithMoreParams, params);
  },
  events: {
    "focus #comment_form_textarea": function(e) { // maybe on keyup ?
      this.showFormControls();
    },
    "blur #comment_form_textarea": function(e) {
      var txt = this.$("#comment_form_textarea").val();
      if (!txt || !txt.length) {
        this.hideFormControls();
      }
    },
    "change #comment_form_textarea": "textChange",
    "keyup #comment_form_textarea": "textChange",
    "paste #comment_form_textarea": "textChange",
    "click #facebookButtonCommentForm" : "facebookClicked",
    "click #twitterButtonCommentForm" : "twitterClicked",
    "click #comment_button": "onSubmitClicked",
  },
  onSubmitClicked: function(e) {
    e.preventDefault();
    this.submitComment();
  },
  submitComment: function(e){
    var that = this;

    function doSubmitComment() {
      if (that.buttonActive) {
        that.buttonActive = false;
        serialize(that, function(attrs){
          that.participantCommented(attrs).then(function() {
            that.$("#comment_form_textarea").val("");
            that.hideFormControls();
            that.showMessage("#comment_sent_message");

          }).always(function() {
            that.buttonActive = true;
          });
        });
      }
    }

    var hasSocial = window.userObject.hasFacebook || window.userObject.hasTwitter;
    if (hasSocial) {
      doSubmitComment();
    } else {
      this.showSocialAuthChoices();
    }
  },
  onAuthSuccess: function() {
    this.reloadPagePreservingCommentText();
    // $("#socialButtonsCommentForm").hide();
    // $("#comment_form_controls").show();
  },
  facebookClicked: function(e) {
    e.preventDefault();
    var that = this;
    PolisFacebookUtils.connect().then(function() {
      // wait a bit for new cookies to be ready, or something, then submit comment.
      setTimeout(function() {
        that.onAuthSuccess();
        // CurrentUserModel.update();
      }, 100);
    }, function(err) {
      // alert("facebook error");
    });
  },
  twitterClicked: function(e) {
    var that = this;
    e.preventDefault();

    eb.on(eb.twitterConnected, function() {
      // wait a bit for new cookies to be ready, or something, then submit comment.
      setTimeout(function() {
        that.onAuthSuccess();
        // CurrentUserModel.update();
      }, 100);
    });
    // open a new window where the twitter auth screen will show.
    // that window will redirect back to a simple page that calls window.opener.twitterStatus("ok")
    var params = 'location=0,status=0,width=800,height=400';
    window.open(document.location.origin + "/api/v3/twitterBtn?dest=/twitterAuthReturn", 'twitterWindow', params);
  },
  showSocialAuthChoices: function() {
    $("#comment_form_controls").hide();
    $("#socialButtonsCommentForm").show();
    $("#socialButtonsUnderReadReact").hide();
  },
  participantCommented: function(attrs) {
    var that = this; //that = the view
    attrs.pid = "mypid";
    attrs.conversation_id = this.conversation_id;
    attrs.vote = constants.REACTIONS.AGREE; // participants' comments are automatically agreed to. Needed for now since math assumes every comment has at least one vote.

    if (/^\s*$/.exec(attrs.txt)) {
      alert("Comment is empty");
      return reject();
    }
    if (attrs.txt.length > 997) {
      alert("Comment is too long");
      return reject();
    }

    // replace newlines with whitespace
    attrs.txt = attrs.txt.replace(/\n/g, " ");

    if (Utils.isDemoMode()) {
      that.showMessage("#comment_sent_message");
      that.trigger("commentSubmitted");
      return resolve();
    }

    var promise = that.serverClient.submitComment(attrs);
    if (!promise) {
      return reject();
    } else {
      promise.then(function() {
        that.trigger("commentSubmitted"); // view.trigger
        // $("#comment_form_textarea").hide();
        // $("#commentSentAlert").fadeIn(300);
        // setTimeout(function() {
        //   $("#commentSentAlert").fadeOut(500, function() {
        //     $("#comment_form_textarea").fadeIn(400);
        //   });
        // }, 1500);
      }, function(err) {
        if (err.status === 409) {

          // that.model.set({
          //   error: "Duplicate!",
          //   errorExtra: "That comment already exists.",
          // });
          alert("Duplicate! That comment already exists.");
        } else if (err.responseText === "polis_err_conversation_is_closed"){

          // that.model.set({
          //   error: "This conversation is closed.",
          //   errorExtra: "No further commenting is allowed.",
          // });
          alert("This conversation is closed. No further commenting is allowed.");
        } else {

          // that.model.set({
          //   error: "Error sending comment.",
          //   errorExtra: "Please try again later.",
          // });
          alert("Error sending comment, please try again later.");
        }
      });
      return promise;
    }
  },
  initialize: function(options) {
    Handlebones.ModelView.prototype.initialize.apply(this, arguments);
    this.model = options.model;
    this.conversation_id = options.conversation_id;
    this.collection = options.collection;
    // this.commentsByMeView = this.addChild(new CommentsByMeView({
    //   collection: options.collection
    // }));
    this.serverClient = options.serverClient;

    this.profilePicView = this.addChild(new ProfilePicView({
      model: CurrentUserModel,
    }));

    if (options.wipCommentFormText) {
      this.shouldAutofocusOnTextarea = true;
    }

    var that = this;
    this.listenTo(this, "render", function(){
      setTimeout(function() {
        if (!_.isUndefined(options.wipCommentFormText)) {
          $("#comment_form_textarea").val(options.wipCommentFormText);
          eb.trigger(eb.doneUsingWipCommentFormText);
          that.submitComment();
        }
        autosize($("#comment_form_textarea"));
      },100);
    });

  },
});
<|endoftext|>


<commit_before>/**
 * Sleep charter, data handlers
 * Roy Curtis, MIT license, 2017
 */

/**
 * Regex pattern for Google Sheets default date/time format (DD/MM/YYYY HH:mm:ss).
 *
 * Capture groups:
 * 1: DD (day)
 * 2: MM (month)
 * 3: YYYY (year)
 * 4: HH (24 hour)
 * 5: mm (minute)
 * 6: ss (second)
 * @type {RegExp}
 */
var GOOGLE_DATETIME_REGEX = /(\d{2}).(\d{2}).(\d{4}) (\d{2}):(\d{2}):(\d{2})/;

/**
 * Parses the given raw CSV data into an array of {@link SleepEvent}s
 *
 * @param {string} csv
 * @returns {SleepEvent[]}
 */
function parseCSV(csv)
{
    var lines = csv.split('\n');

    // Remove CSV header
    lines.shift();
    return lines.map(parseCSVLine);
}

/** @param {string} line */
function parseCSVLine(line)
{
    var ends  = line.split(','),
        begin = parseDate(ends[0]),
        end   = parseDate(ends[1]);

    // Validate date pair
    if (end.getTime() - begin.getTime() <= 0)
        throw new Error("End time is before begin time: " + line);

    return [begin, end];
}

/**
 * Parse Google Sheets default date format (DD/MM/YYYY HH:mm:ss) into Date object
 *
 * @param {string} date
 * @return {Date}
 */
function parseDate(date)
{
    var matches = date.match(GOOGLE_DATETIME_REGEX);

    if (!matches)
        throw new Error("Date/time failed to parse: " + date);
    else if (matches.index !== 0 || matches.length !== 7)
        throw new Error("Date/time parsed incorrectly: " + date);

    return new Date(
        matches[3], matches[2] - 1, matches[1],
        matches[4], matches[5], matches[6]
    );
}

/**
 * Takes a small amount of the end of the given entries, then timeshifts them by a certain
 * amount of days, and adds them to the end
 *
 * @param {SleepEvent[]} data
 * @return {SleepEvent[]}
 */
function predictEvents(data)
{
    var MAX_PREDICTED_EVENTS = 30;

    /** @type SleepEvent[] */
    var future = data.slice(-MAX_PREDICTED_EVENTS);

    if (future.length < MAX_PREDICTED_EVENTS)
        throw new Error("Not enough events to predict the future...");

    var first = future[0],
        last  = future[MAX_PREDICTED_EVENTS - 1];

    // First, find difference in days between first and last...
    var daysDiff = getDaysSinceEpoch(last[0]) - getDaysSinceEpoch(first[0]);

    for (var i = 0; i < future.length; i++)
    {
        // Make copies of the tuples and dates...
        future[i]    = future[i].slice();
        future[i][0] = new Date( future[i][0].valueOf() );
        future[i][1] = new Date( future[i][1].valueOf() );

        // Shift by difference in dates
        future[i][0].setDate(future[i][0].getDate() + daysDiff + 1);
        future[i][1].setDate(future[i][1].getDate() + daysDiff + 1);

        // Shift by a few hours...
        // TODO: lazily done. smarter way to do this? needs adjusting if MAX changes
        future[i][0].setHours(future[i][0].getHours() + 3);
        future[i][1].setHours(future[i][1].getHours() + 3);

        future[i].isPrediction = true;
        data.push(future[i]);
    }

    return data;
}<commit_msg>Shift predictions back by one day<commit_after>/**
 * Sleep charter, data handlers
 * Roy Curtis, MIT license, 2017
 */

/**
 * Regex pattern for Google Sheets default date/time format (DD/MM/YYYY HH:mm:ss).
 *
 * Capture groups:
 * 1: DD (day)
 * 2: MM (month)
 * 3: YYYY (year)
 * 4: HH (24 hour)
 * 5: mm (minute)
 * 6: ss (second)
 * @type {RegExp}
 */
var GOOGLE_DATETIME_REGEX = /(\d{2}).(\d{2}).(\d{4}) (\d{2}):(\d{2}):(\d{2})/;

/**
 * Parses the given raw CSV data into an array of {@link SleepEvent}s
 *
 * @param {string} csv
 * @returns {SleepEvent[]}
 */
function parseCSV(csv)
{
    var lines = csv.split('\n');

    // Remove CSV header
    lines.shift();
    return lines.map(parseCSVLine);
}

/** @param {string} line */
function parseCSVLine(line)
{
    var ends  = line.split(','),
        begin = parseDate(ends[0]),
        end   = parseDate(ends[1]);

    // Validate date pair
    if (end.getTime() - begin.getTime() <= 0)
        throw new Error("End time is before begin time: " + line);

    return [begin, end];
}

/**
 * Parse Google Sheets default date format (DD/MM/YYYY HH:mm:ss) into Date object
 *
 * @param {string} date
 * @return {Date}
 */
function parseDate(date)
{
    var matches = date.match(GOOGLE_DATETIME_REGEX);

    if (!matches)
        throw new Error("Date/time failed to parse: " + date);
    else if (matches.index !== 0 || matches.length !== 7)
        throw new Error("Date/time parsed incorrectly: " + date);

    return new Date(
        matches[3], matches[2] - 1, matches[1],
        matches[4], matches[5], matches[6]
    );
}

/**
 * Takes a small amount of the end of the given entries, then timeshifts them by a certain
 * amount of days, and adds them to the end
 *
 * @param {SleepEvent[]} data
 * @return {SleepEvent[]}
 */
function predictEvents(data)
{
    var MAX_PREDICTED_EVENTS = 30;

    /** @type SleepEvent[] */
    var future = data.slice(-MAX_PREDICTED_EVENTS);

    if (future.length < MAX_PREDICTED_EVENTS)
        throw new Error("Not enough events to predict the future...");

    var first = future[0],
        last  = future[MAX_PREDICTED_EVENTS - 1];

    // First, find difference in days between first and last...
    var daysDiff = getDaysSinceEpoch(last[0]) - getDaysSinceEpoch(first[0]);

    for (var i = 0; i < future.length; i++)
    {
        // Make copies of the tuples and dates...
        future[i]    = future[i].slice();
        future[i][0] = new Date( future[i][0].valueOf() );
        future[i][1] = new Date( future[i][1].valueOf() );

        // Shift by difference in dates
        future[i][0].setDate(future[i][0].getDate() + daysDiff);
        future[i][1].setDate(future[i][1].getDate() + daysDiff);

        // Shift by a few hours...
        // TODO: lazily done. smarter way to do this? needs adjusting if MAX changes
        future[i][0].setHours(future[i][0].getHours() + 3);
        future[i][1].setHours(future[i][1].getHours() + 3);

        future[i].isPrediction = true;
        data.push(future[i]);
    }

    return data;
}<|endoftext|>


<commit_before>/* eslint-disable no-case-declarations, no-restricted-modules */

const os = require('os');
const cluster = require('cluster');
const EventEmitter = require('events').EventEmitter;
const oe = require('obj-ease');

let time;
let timeEnd;

class Core extends EventEmitter {
    constructor(config, log) {
        super();
        config = config || {};
        this.log = log;
        this.db = {};
        this.queries = {};
        this.views = {};
        this.viewsEnv = {};
        this.name = config.name || 'db';
        this.backend = config.backend || 'file';
        this.scriptTimeout = config.scriptTimeout || 10000;

        if (config.verbosity === 'debug') {
            time = console.time;
            timeEnd = console.timeEnd;
        } else {
            time = () => {};
            timeEnd = () => {};
        }

        require('./backend/' + this.backend + '.js')(this);

        this.workers = [];
        this.numWorkers = config.workers || os.cpus().length;

        cluster.setupMaster({
            exec: 'lib/worker.js',
            args: [],
            silent: false
        });

        cluster.on('online', worker => {
            this.workers.push(worker);
            log.info('worker ' + worker.process.pid + ' is online');
            worker.on('message', msg => {
                switch (msg.type) {
                    case 'view':
                        if (msg.payload === '') {
                            delete this.views[msg.id];
                        } else {
                            this.views[msg.id] = msg.payload;
                        }
                        this.emit('view', msg.id, msg.payload);
                        break;
                    default:
                        console.log(msg);
                }
            });
            if (this.workers.length === this.numWorkers) {
                log.info('all workers ready');
                startInit();
            }
        });

        const env = {};
        if (config.verbosity === 'debug') {
            env.DEBUG = '1';
        }
        for (let i = 0; i < this.numWorkers; i++) {
            cluster.fork(env);
        }

        this.on('update', (id, payload) => {
            this.workers.forEach(worker => {
                time('sending update to worker ' + worker.process.pid);
                worker.send({type: 'update', id, payload, rev: this.rev});
                timeEnd('sending update to worker ' + worker.process.pid);
            });
        });

        this.on('ready', () => {
            startInit();
        });

        let eventCount = 0;
        function startInit() {
            eventCount += 1;
            if (eventCount === 2) {
                init();
            }
        }

        const that = this;

        function init() {
            that.workers.forEach(worker => {
                time('sending db to worker ' + worker.process.pid);
                worker.send({type: 'db', db: that.db, rev: that.rev});
                timeEnd('sending db to worker ' + worker.process.pid);
            });
            Object.keys(that.queries).forEach(id => {
                that.query(id, that.queries[id], true);
            });
            log.info('init complete');
        }
    }

    getRev(id) {
        if (this.db[id] && (typeof this.db[id]._rev !== 'undefined')) {
            const rev = this.db[id]._rev;
            delete this.db[id]._rev;
            return rev;
        }
        return -1;
    }

    setRev(id, rev) {
        if (this.db[id]) {
            this.db[id]._rev = rev;
        }
    }

    incRev(id, rev) {
        if (this.db[id]) {
            this.db[id]._rev = rev + 1;
        }
    }

    set(id, payload) {
        if (payload === '') {
            return this.del(id);
        }
        if (typeof payload !== 'object') {
            return false;
        }
        delete payload._rev;
        delete payload._id;
        const rev = this.getRev(id);
        if (this.db[id]) {
            delete this.db[id]._id;
        }
        if (!oe.equal(this.db[id], payload)) {
            this.db[id] = payload;
            this.db[id]._id = id;
            this.incRev(id, rev);
            this.emit('update', id, this.db[id]);
            return true;
        }
        this.db[id]._id = id;

        this.setRev(id, rev);
        return false;
    }

    prop(id, payload) {
        let rev;
        switch (payload.method) {
            case 'set':
                rev = this.getRev(id);
                if (oe.setProp(this.db[id], payload.prop, payload.val)) {
                    delete this.db[id]._id;
                    this.db[id]._id = id;
                    this.incRev(id, rev);
                    this.emit('update', id, this.db[id]);
                    return true;
                }
                this.setRev(id, rev);
                return false;

            case 'create':
                if (typeof oe.getProp(this.db[id], payload.prop) === 'undefined') {
                    rev = this.getRev(id);
                    oe.setProp(this.db[id], payload.prop, payload.val);
                    this.db[id]._id = id;
                    this.incRev(id, rev);
                    this.emit('update', id, this.db[id]);
                    return true;
                }
                return false;

            case 'del':
                rev = this.getRev(id);
                if (oe.delProp(this.db[id], payload.prop)) {
                    this.db[id]._id = id;
                    this.incRev(id, rev);
                    this.emit('update', id, this.db[id]);
                    return true;
                }
                this.setRev(id, rev);
                return false;

            default:
                console.log('unknown prop method', payload.method);
        }
    }

    extend(id, payload) {
        if (!payload || typeof payload !== 'object') {
            return false;
        }
        const rev = this.getRev(id);
        delete payload._id;
        delete payload._rev;
        if (this.db[id]) {
            delete this.db[id]._id;
        } else {
            this.db[id] = {};
        }
        const change = oe.extend(this.db[id], payload);
        this.db[id]._id = id;
        if (change) {
            this.incRev(id, rev);
            this.emit('update', id, this.db[id]);
            return true;
        }
        this.setRev(id, rev);
        return false;
    }

    del(id) {
        delete this.db[id];
        this.emit('update', id, '');
    }

    getWorker() {
        this.nextWorker = this.nextWorker || 0;
        const id = this.nextWorker;
        this.nextWorker += 1;
        if (this.nextWorker >= this.numWorkers) {
            this.nextWorker = 0;
        }
        return id;
    }

    query(id, payload, init) {
        if (!this.viewsEnv[id]) {
            this.viewsEnv[id] = {};
        }
        let workerId;
        if (!this.viewsEnv[id] || typeof this.viewsEnv[id].worker === 'undefined' || this.viewsEnv[id].worker >= this.numWorkers) {
            workerId = this.getWorker();
            this.viewsEnv[id].worker = workerId;
        } else {
            workerId = this.viewsEnv[id].worker;
        }

        if (payload === '') {
            delete this.queries[id];
            delete this.viewsEnv[id];
            delete this.views[id];
        } else {
            this.queries[id] = payload;
        }

        if (!init) {
            this.emit('query');
        }

        this.log.debug('sending query', id, 'to worker', workerId, payload);
        this.workers[workerId].send({type: 'query', id, payload, init: init ? this.views[id] : undefined});
    }

}

module.exports = Core;
<commit_msg>improve coverage<commit_after>/* eslint-disable no-case-declarations, no-restricted-modules */

const os = require('os');
const cluster = require('cluster');
const EventEmitter = require('events').EventEmitter;
const oe = require('obj-ease');

let time;
let timeEnd;

class Core extends EventEmitter {
    constructor(config, log) {
        super();
        /* istanbul ignore next */
        config = config || {};
        this.log = log;
        this.db = {};
        this.queries = {};
        this.views = {};
        this.viewsEnv = {};
        /* istanbul ignore next */
        this.name = config.name || 'db';
        /* istanbul ignore next */
        this.backend = config.backend || 'file';
        /* istanbul ignore next */
        this.scriptTimeout = config.scriptTimeout || 10000;

        /* istanbul ignore else */
        if (config.verbosity === 'debug') {
            time = console.time;
            timeEnd = console.timeEnd;
        } else {
            time = () => {};
            timeEnd = () => {};
        }

        require('./backend/' + this.backend + '.js')(this);

        this.workers = [];

        /* istanbul ignore next */
        this.numWorkers = config.workers || os.cpus().length;

        cluster.setupMaster({
            exec: 'lib/worker.js',
            args: [],
            silent: false
        });

        cluster.on('online', worker => {
            this.workers.push(worker);
            log.info('worker ' + worker.process.pid + ' is online');
            worker.on('message', msg => {
                switch (msg.type) {
                    case 'view':
                        if (msg.payload === '') {
                            delete this.views[msg.id];
                        } else {
                            this.views[msg.id] = msg.payload;
                        }
                        this.emit('view', msg.id, msg.payload);
                        break;

                    default:
                }
            });
            if (this.workers.length === this.numWorkers) {
                log.info('all workers ready');
                startInit();
            }
        });

        const env = {};

        /* istanbul ignore else */
        if (config.verbosity === 'debug') {
            env.DEBUG = '1';
        }
        for (let i = 0; i < this.numWorkers; i++) {
            cluster.fork(env);
        }

        this.on('update', (id, payload) => {
            this.workers.forEach(worker => {
                time('sending update to worker ' + worker.process.pid);
                worker.send({type: 'update', id, payload, rev: this.rev});
                timeEnd('sending update to worker ' + worker.process.pid);
            });
        });

        this.on('ready', () => {
            startInit();
        });

        let eventCount = 0;
        function startInit() {
            eventCount += 1;
            if (eventCount === 2) {
                init();
            }
        }

        const that = this;

        function init() {
            that.workers.forEach(worker => {
                time('sending db to worker ' + worker.process.pid);
                worker.send({type: 'db', db: that.db, rev: that.rev});
                timeEnd('sending db to worker ' + worker.process.pid);
            });
            Object.keys(that.queries).forEach(id => {
                that.query(id, that.queries[id], true);
            });
            log.info('init complete');
        }
    }

    getRev(id) {
        if (this.db[id] && (typeof this.db[id]._rev !== 'undefined')) {
            const rev = this.db[id]._rev;
            delete this.db[id]._rev;
            return rev;
        }
        return -1;
    }

    setRev(id, rev) {
        if (this.db[id]) {
            this.db[id]._rev = rev;
        }
    }

    incRev(id, rev) {
        if (this.db[id]) {
            this.db[id]._rev = rev + 1;
        }
    }

    set(id, payload) {
        if (payload === '') {
            return this.del(id);
        }
        if (typeof payload !== 'object') {
            return false;
        }
        delete payload._rev;
        delete payload._id;
        const rev = this.getRev(id);
        if (this.db[id]) {
            delete this.db[id]._id;
        }
        if (!oe.equal(this.db[id], payload)) {
            this.db[id] = payload;
            this.db[id]._id = id;
            this.incRev(id, rev);
            this.emit('update', id, this.db[id]);
            return true;
        }
        this.db[id]._id = id;

        this.setRev(id, rev);
        return false;
    }

    prop(id, payload) {
        let rev;
        switch (payload.method) {
            case 'set':
                rev = this.getRev(id);
                if (oe.setProp(this.db[id], payload.prop, payload.val)) {
                    delete this.db[id]._id;
                    this.db[id]._id = id;
                    this.incRev(id, rev);
                    this.emit('update', id, this.db[id]);
                    return true;
                }
                this.setRev(id, rev);
                return false;

            case 'create':
                if (typeof oe.getProp(this.db[id], payload.prop) === 'undefined') {
                    rev = this.getRev(id);
                    oe.setProp(this.db[id], payload.prop, payload.val);
                    this.db[id]._id = id;
                    this.incRev(id, rev);
                    this.emit('update', id, this.db[id]);
                    return true;
                }
                return false;

            case 'del':
                rev = this.getRev(id);
                if (oe.delProp(this.db[id], payload.prop)) {
                    this.db[id]._id = id;
                    this.incRev(id, rev);
                    this.emit('update', id, this.db[id]);
                    return true;
                }
                this.setRev(id, rev);
                return false;

            default:
                console.log('unknown prop method', payload.method);
        }
    }

    extend(id, payload) {
        if (!payload || typeof payload !== 'object') {
            return false;
        }
        const rev = this.getRev(id);
        delete payload._id;
        delete payload._rev;
        if (this.db[id]) {
            delete this.db[id]._id;
        } else {
            this.db[id] = {};
        }
        const change = oe.extend(this.db[id], payload);
        this.db[id]._id = id;
        if (change) {
            this.incRev(id, rev);
            this.emit('update', id, this.db[id]);
            return true;
        }
        this.setRev(id, rev);
        return false;
    }

    del(id) {
        delete this.db[id];
        this.emit('update', id, '');
    }

    getWorker() {
        this.nextWorker = this.nextWorker || 0;
        const id = this.nextWorker;
        this.nextWorker += 1;
        if (this.nextWorker >= this.numWorkers) {
            this.nextWorker = 0;
        }
        return id;
    }

    query(id, payload, init) {
        if (!this.viewsEnv[id]) {
            this.viewsEnv[id] = {};
        }
        let workerId;
        if (!this.viewsEnv[id] || typeof this.viewsEnv[id].worker === 'undefined' || this.viewsEnv[id].worker >= this.numWorkers) {
            workerId = this.getWorker();
            this.viewsEnv[id].worker = workerId;
        } else {
            workerId = this.viewsEnv[id].worker;
        }

        if (payload === '') {
            delete this.queries[id];
            delete this.viewsEnv[id];
            delete this.views[id];
        } else {
            this.queries[id] = payload;
        }

        if (!init) {
            this.emit('query');
        }

        this.log.debug('sending query', id, 'to worker', workerId, payload);
        this.workers[workerId].send({type: 'query', id, payload, init: init ? this.views[id] : undefined});
    }

}

module.exports = Core;
<|endoftext|>