<commit_before>// @flow

import React, {Component} from 'react'
import moment from 'moment'

import * as tripActions from '../../actions/trip'
import {secondsAfterMidnightToHHMM} from '../../../common/util/gtfs'
import { isTimeFormat, TIMETABLE_FORMATS } from '../../util/timetable'

import type {TimetableColumn} from '../../../types'
import type {EditorValidationIssue} from '../../util/validation'

export type CellData = ?(number | string)

type Props = {
  column: TimetableColumn,
  columnIndex: number,
  data: CellData,
  handlePastedRows: (Array<any>, number, number) => void,
  invalidData: ?EditorValidationIssue,
  isEditing: boolean,
  isFocused: boolean,
  isSelected: boolean,
  lightText: boolean,
  offsetScrollCol: number => void,
  offsetScrollRow: number => void,
  onChange: (?(number | string), number, TimetableColumn, number) => void,
  onClick: (number, number) => void,
  onStopEditing: () => void,
  placeholder: ?string,
  rowIndex: number,
  setActiveCell: typeof tripActions.setActiveCell,
  style: {[string]: string | number}
}

type State = {
  data: CellData,
  edited: boolean,
  isEditing: boolean,
  isFocused: boolean,
  originalData: CellData
}

const renderCell = (
  col: TimetableColumn,
  value: ?(number | string)
): number | string => {
  if (!isTimeFormat(col.type)) {
    // If not a time format, return string value (or empty string to avoid null)
    return value || ''
  } else {
    return secondsAfterMidnightToHHMM(value)
  }
}

/**
 * A component to handle the editing of a cell in a timetable editor
 */
export default class EditableCell extends Component<Props, State> {
  componentWillMount () {
    this.setState({
      isEditing: this.props.isEditing,
      isFocused: false,
      edited: false,
      data: this.props.data,
      originalData: this.props.data
    })
  }

  cellInput = null

  /**
   * The component may receive data from a save event or
   * editing can be changed by a change in the activeCell in the TimetableGrid
   */
  componentWillReceiveProps (nextProps: Props) {
    if (this.props.data !== nextProps.data) {
      this.setState({data: nextProps.data})
    }
    if (this.state.isEditing !== nextProps.isEditing) {
      this.setState({isEditing: nextProps.isEditing})
    }
  }

  cancel = () => {
    this.setState({
      isEditing: false,
      isFocused: false,
      data: this.props.data
    })
    this.props.onStopEditing()
  }

  beginEditing () {
    const {columnIndex, rowIndex, setActiveCell} = this.props
    setActiveCell(`${rowIndex}-${columnIndex}`)
    this.setState({isEditing: true})
  }

  handleClick = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    const {columnIndex, isFocused, onClick, rowIndex} = this.props
    if (isFocused) this.beginEditing()
    else onClick(rowIndex, columnIndex)
  }

  /**
   * Depending on the key pressed while focused on a cell, do some special things.
   */
  handleKeyDown = (evt: SyntheticKeyboardEvent<HTMLInputElement>) => {
    const {
      isFocused,
      offsetScrollCol,
      offsetScrollRow
    } = this.props
    const {isEditing} = this.state
    switch (evt.keyCode) {
      case 13: // Enter
        evt.preventDefault()
        if (isFocused) {
          this.beginEditing()
        }
        // handle shift
        if (evt.shiftKey) {
          this.save()
          offsetScrollRow(-1)
        } else {
          this.save()
        }
        break
      case 9: // Tab
        // save and advance to next cell if editing
        this.save()
        offsetScrollCol(evt.shiftKey ? -1 : 1)
        evt.preventDefault()
        evt.stopPropagation()
        break
      case 27: // Esc
        this.cancel()
        break
      case 39: // right
        // cancel event propogation if cell is being edited
        if (isEditing) {
          evt.stopPropagation()
          return
        }

        // update scroll position in TimetableGrid
        offsetScrollCol(1)
        return
      case 37: // left
        // do nothing if cell is being edited
        if (isEditing) {
          evt.stopPropagation()
          return
        }

        // update scroll position in TimetableGrid
        offsetScrollCol(-1)
        return
      case 38: // up
        this.save()
        break
      case 40: // down
        this.save()
        break
      default:
        return true
    }
  }

  _onOuterKeyDown = (e: SyntheticKeyboardEvent<HTMLInputElement>) => {
    const {offsetScrollCol} = this.props
    switch (e.keyCode) {
      case 9: // tab
        // update scroll position in TimetableGrid
        offsetScrollCol(e.shiftKey ? -1 : 1)
        // prevent other listeners and default browser tabbing
        e.stopPropagation()
        e.preventDefault()
        break
      case 37: // left
        // update scroll position in TimetableGrid
        offsetScrollCol(-1)
        break
      case 39: // right
        // update scroll position in TimetableGrid
        offsetScrollCol(1)
        break
    }
  }

  _handleSave = (value: any) => {
    const { rowIndex, column, columnIndex, onChange, onStopEditing } = this.props
    this.setState({isEditing: false, data: value})
    onChange(value, rowIndex, column, columnIndex)
    onStopEditing()
  }

  save () {
    const {column} = this.props
    const {data} = this.state
    // for non-time rendering
    if (column.type === 'TEXT') {
      if (data !== this.state.originalData) this._handleSave(data)
      else this.cancel()
    } else if (column.type === 'SECONDS') {
      // Ensure that only a positive integer value can be set.
      const value = +data
      this._handleSave(value)
    } else {
      // for time rendering
      // FIXME replace with secondsAfterMidnightToHHMM?
      if (typeof data !== 'string') return this.cancel()
      let time = data
      let hours = 0
      let parts
      if (time.indexOf(':') > -1) {
        // is a string with colons
        parts = time.split(':')
        hours = +parts[0]
      } else {
        // is a string without colons
        if (time.length === 3) {
          // format of `hmm`
          parts = [time[0], time.substr(1, 3)]
          hours = +time[0]
        } else if (time.length === 4) {
          // format of `HHmm`
          parts = [time.substr(0, 2), time.substr(2, 4)]
          hours = +time.substr(0, 2)
        }
      }

      let greaterThan24 = false

      // check for times greater than 24:00
      // TODO: fix edge case of a stop time being 34 hours or more
      // realistically this probably wouldn't happen, but a hypothetical
      // case for this would be an airline flight that leaves at 23:59
      // and flies for 13 hours before landing.  Another use-case is the
      // trans-siberian railway that takes 7 days to travel from Moscow to
      // Beijing.
      if (parts && parts[0] && hours >= 24 && hours < 34) {
        parts[0] = `0${hours - 24}`
        greaterThan24 = true
        time = parts.join(':')
      }
      const date = moment().startOf('day').format('YYYY-MM-DD')
      const momentTime = moment(date + 'T' + time, TIMETABLE_FORMATS, true)
      let value = momentTime.isValid() ? momentTime.diff(date, 'seconds') : null

      if (greaterThan24 && momentTime.isValid()) {
        value += 86400
      }
      // Check for valid time and new value
      const timeIsValid = (time === '' || momentTime.isValid()) && value !== time
      if (timeIsValid) this._handleSave(value)
      else this.cancel()
    }
  }

  handleChange = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this.setState({data: evt.target.value})
  }

  handlePaste = (evt: ClipboardEvent) => {
    const {handlePastedRows, rowIndex, columnIndex} = this.props
    const {clipboardData} = evt
    if (!clipboardData) {
      console.warn('No clipboard data found.')
      return
    }
    const text = clipboardData.getData('Text')
    const rowDelimiter = text.indexOf('\n') > -1 // google sheets row delimiter
      ? '\n'
      : text.indexOf(String.fromCharCode(13)) > -1 // excel row delimiter
        ? String.fromCharCode(13)
        : undefined
    const rows = text.split(rowDelimiter)
    const rowsAndColumns = []
    // Split each row into columns
    for (let i = 0; i < rows.length; i++) {
      rowsAndColumns.push(rows[i].split(String.fromCharCode(9)))
    }

    if (rowsAndColumns.length > 1 || rowsAndColumns[0].length > 1) {
      this.cancel()
      handlePastedRows(rowsAndColumns, rowIndex, columnIndex)
      evt.preventDefault()
    }
  }

  _onInputFocus = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    evt.target.select()
  }

  render () {
    const {column, invalidData, isFocused, isSelected, lightText, placeholder, style} = this.props
    const {data, edited, isEditing} = this.state
    const rowCheckedColor = '#F3FAF6'
    const focusedNotEditing = isFocused && !isEditing
    const edgeDiff = isFocused ? 0 : 0.5
    const divStyle = {
      paddingTop: `${3 + edgeDiff}px`,
      paddingLeft: `${3 + edgeDiff}px`,
      fontWeight: edited ? 'bold' : 'normal'
    }
    const cellStyle = {
      backgroundColor: invalidData && !isEditing
        ? 'pink'
        : focusedNotEditing
          ? '#F4F4F4'
          : isEditing
            ? '#fff'
            : isSelected
              ? rowCheckedColor
              : '#fff',
      border: invalidData && focusedNotEditing
        ? '2px solid red'
        : isFocused
          ? `2px solid #66AFA2`
          : '1px solid #ddd',
      margin: `${-0.5 + edgeDiff}px`,
      padding: `${-edgeDiff}px`,
      // fontFamily: '"Courier New", Courier, monospace',
      color: lightText ? '#aaa' : '#000',
      ...style
    }
    const cellHtml = isEditing
      ? <input
        defaultValue={renderCell(column, data)}
        className='cell-input'
        onBlur={this.cancel}
        onChange={this.handleChange}
        onFocus={this._onInputFocus}
        onKeyDown={this.handleKeyDown}
        onPaste={this.handlePaste}
        placeholder={placeholder || ''}
        readOnly={!isEditing}
        type='text' />
      : <div
        className='cell-div noselect'
        style={divStyle}>
        {renderCell(column, data)}
      </div>
    return (
      <div
        className='editable-cell small'
        role='button'
        title={!isEditing && invalidData
          ? invalidData.reason
          : undefined
        }
        style={cellStyle}
        tabIndex={0}
        onClick={this.handleClick}
        onKeyDown={this._onOuterKeyDown}>
        {cellHtml}
      </div>
    )
  }
}
<commit_msg>fix(editor): re-enable autofocus of editable cell inputs<commit_after>// @flow

import React, {Component} from 'react'
import moment from 'moment'

import * as tripActions from '../../actions/trip'
import {secondsAfterMidnightToHHMM} from '../../../common/util/gtfs'
import { isTimeFormat, TIMETABLE_FORMATS } from '../../util/timetable'

import type {TimetableColumn} from '../../../types'
import type {EditorValidationIssue} from '../../util/validation'

export type CellData = ?(number | string)

type Props = {
  column: TimetableColumn,
  columnIndex: number,
  data: CellData,
  handlePastedRows: (Array<any>, number, number) => void,
  invalidData: ?EditorValidationIssue,
  isEditing: boolean,
  isFocused: boolean,
  isSelected: boolean,
  lightText: boolean,
  offsetScrollCol: number => void,
  offsetScrollRow: number => void,
  onChange: (?(number | string), number, TimetableColumn, number) => void,
  onClick: (number, number) => void,
  onStopEditing: () => void,
  placeholder: ?string,
  rowIndex: number,
  setActiveCell: typeof tripActions.setActiveCell,
  style: {[string]: string | number}
}

type State = {
  data: CellData,
  edited: boolean,
  isEditing: boolean,
  isFocused: boolean,
  originalData: CellData
}

const renderCell = (
  col: TimetableColumn,
  value: ?(number | string)
): number | string => {
  if (!isTimeFormat(col.type)) {
    // If not a time format, return string value (or empty string to avoid null)
    return value || ''
  } else {
    return secondsAfterMidnightToHHMM(value)
  }
}

/**
 * A component to handle the editing of a cell in a timetable editor
 */
export default class EditableCell extends Component<Props, State> {
  componentWillMount () {
    this.setState({
      isEditing: this.props.isEditing,
      isFocused: false,
      edited: false,
      data: this.props.data,
      originalData: this.props.data
    })
  }

  cellInput = null

  /**
   * The component may receive data from a save event or
   * editing can be changed by a change in the activeCell in the TimetableGrid
   */
  componentWillReceiveProps (nextProps: Props) {
    if (this.props.data !== nextProps.data) {
      this.setState({data: nextProps.data})
    }
    if (this.state.isEditing !== nextProps.isEditing) {
      this.setState({isEditing: nextProps.isEditing})
    }
  }

  cancel = () => {
    this.setState({
      isEditing: false,
      isFocused: false,
      data: this.props.data
    })
    this.props.onStopEditing()
  }

  beginEditing () {
    const {columnIndex, rowIndex, setActiveCell} = this.props
    setActiveCell(`${rowIndex}-${columnIndex}`)
    this.setState({isEditing: true})
  }

  handleClick = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    const {columnIndex, isFocused, onClick, rowIndex} = this.props
    if (isFocused) this.beginEditing()
    else onClick(rowIndex, columnIndex)
  }

  /**
   * Depending on the key pressed while focused on a cell, do some special things.
   */
  handleKeyDown = (evt: SyntheticKeyboardEvent<HTMLInputElement>) => {
    const {
      isFocused,
      offsetScrollCol,
      offsetScrollRow
    } = this.props
    const {isEditing} = this.state
    switch (evt.keyCode) {
      case 13: // Enter
        evt.preventDefault()
        if (isFocused) {
          this.beginEditing()
        }
        // handle shift
        if (evt.shiftKey) {
          this.save()
          offsetScrollRow(-1)
        } else {
          this.save()
        }
        break
      case 9: // Tab
        // save and advance to next cell if editing
        this.save()
        offsetScrollCol(evt.shiftKey ? -1 : 1)
        evt.preventDefault()
        evt.stopPropagation()
        break
      case 27: // Esc
        this.cancel()
        break
      case 39: // right
        // cancel event propogation if cell is being edited
        if (isEditing) {
          evt.stopPropagation()
          return
        }

        // update scroll position in TimetableGrid
        offsetScrollCol(1)
        return
      case 37: // left
        // do nothing if cell is being edited
        if (isEditing) {
          evt.stopPropagation()
          return
        }

        // update scroll position in TimetableGrid
        offsetScrollCol(-1)
        return
      case 38: // up
        this.save()
        break
      case 40: // down
        this.save()
        break
      default:
        return true
    }
  }

  _onOuterKeyDown = (e: SyntheticKeyboardEvent<HTMLInputElement>) => {
    const {offsetScrollCol} = this.props
    switch (e.keyCode) {
      case 9: // tab
        // update scroll position in TimetableGrid
        offsetScrollCol(e.shiftKey ? -1 : 1)
        // prevent other listeners and default browser tabbing
        e.stopPropagation()
        e.preventDefault()
        break
      case 37: // left
        // update scroll position in TimetableGrid
        offsetScrollCol(-1)
        break
      case 39: // right
        // update scroll position in TimetableGrid
        offsetScrollCol(1)
        break
    }
  }

  _handleSave = (value: any) => {
    const { rowIndex, column, columnIndex, onChange, onStopEditing } = this.props
    this.setState({isEditing: false, data: value})
    onChange(value, rowIndex, column, columnIndex)
    onStopEditing()
  }

  save () {
    const {column} = this.props
    const {data} = this.state
    // for non-time rendering
    if (column.type === 'TEXT') {
      if (data !== this.state.originalData) this._handleSave(data)
      else this.cancel()
    } else if (column.type === 'SECONDS') {
      // Ensure that only a positive integer value can be set.
      const value = +data
      this._handleSave(value)
    } else {
      // for time rendering
      // FIXME replace with secondsAfterMidnightToHHMM?
      if (typeof data !== 'string') return this.cancel()
      let time = data
      let hours = 0
      let parts
      if (time.indexOf(':') > -1) {
        // is a string with colons
        parts = time.split(':')
        hours = +parts[0]
      } else {
        // is a string without colons
        if (time.length === 3) {
          // format of `hmm`
          parts = [time[0], time.substr(1, 3)]
          hours = +time[0]
        } else if (time.length === 4) {
          // format of `HHmm`
          parts = [time.substr(0, 2), time.substr(2, 4)]
          hours = +time.substr(0, 2)
        }
      }

      let greaterThan24 = false

      // check for times greater than 24:00
      // TODO: fix edge case of a stop time being 34 hours or more
      // realistically this probably wouldn't happen, but a hypothetical
      // case for this would be an airline flight that leaves at 23:59
      // and flies for 13 hours before landing.  Another use-case is the
      // trans-siberian railway that takes 7 days to travel from Moscow to
      // Beijing.
      if (parts && parts[0] && hours >= 24 && hours < 34) {
        parts[0] = `0${hours - 24}`
        greaterThan24 = true
        time = parts.join(':')
      }
      const date = moment().startOf('day').format('YYYY-MM-DD')
      const momentTime = moment(date + 'T' + time, TIMETABLE_FORMATS, true)
      let value = momentTime.isValid() ? momentTime.diff(date, 'seconds') : null

      if (greaterThan24 && momentTime.isValid()) {
        value += 86400
      }
      // Check for valid time and new value
      const timeIsValid = (time === '' || momentTime.isValid()) && value !== time
      if (timeIsValid) this._handleSave(value)
      else this.cancel()
    }
  }

  handleChange = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this.setState({data: evt.target.value})
  }

  handlePaste = (evt: ClipboardEvent) => {
    const {handlePastedRows, rowIndex, columnIndex} = this.props
    const {clipboardData} = evt
    if (!clipboardData) {
      console.warn('No clipboard data found.')
      return
    }
    const text = clipboardData.getData('Text')
    const rowDelimiter = text.indexOf('\n') > -1 // google sheets row delimiter
      ? '\n'
      : text.indexOf(String.fromCharCode(13)) > -1 // excel row delimiter
        ? String.fromCharCode(13)
        : undefined
    const rows = text.split(rowDelimiter)
    const rowsAndColumns = []
    // Split each row into columns
    for (let i = 0; i < rows.length; i++) {
      rowsAndColumns.push(rows[i].split(String.fromCharCode(9)))
    }

    if (rowsAndColumns.length > 1 || rowsAndColumns[0].length > 1) {
      this.cancel()
      handlePastedRows(rowsAndColumns, rowIndex, columnIndex)
      evt.preventDefault()
    }
  }

  _onInputFocus = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    evt.target.select()
  }

  render () {
    const {column, invalidData, isFocused, isSelected, lightText, placeholder, style} = this.props
    const {data, edited, isEditing} = this.state
    const rowCheckedColor = '#F3FAF6'
    const focusedNotEditing = isFocused && !isEditing
    const edgeDiff = isFocused ? 0 : 0.5
    const divStyle = {
      paddingTop: `${3 + edgeDiff}px`,
      paddingLeft: `${3 + edgeDiff}px`,
      fontWeight: edited ? 'bold' : 'normal'
    }
    const cellStyle = {
      backgroundColor: invalidData && !isEditing
        ? 'pink'
        : focusedNotEditing
          ? '#F4F4F4'
          : isEditing
            ? '#fff'
            : isSelected
              ? rowCheckedColor
              : '#fff',
      border: invalidData && focusedNotEditing
        ? '2px solid red'
        : isFocused
          ? `2px solid #66AFA2`
          : '1px solid #ddd',
      margin: `${-0.5 + edgeDiff}px`,
      padding: `${-edgeDiff}px`,
      // fontFamily: '"Courier New", Courier, monospace',
      color: lightText ? '#aaa' : '#000',
      ...style
    }
    const cellHtml = isEditing
      ? <input
        /* enable autofocus against jsx-a11y`s best judget so the inputs behave
          like a spreadsheet where cells are automatically in edit mode when
          selected */
        /* eslint-disable-next-line jsx-a11y/no-autofocus */
        autoFocus
        defaultValue={renderCell(column, data)}
        className='cell-input'
        onBlur={this.cancel}
        onChange={this.handleChange}
        onFocus={this._onInputFocus}
        onKeyDown={this.handleKeyDown}
        onPaste={this.handlePaste}
        placeholder={placeholder || ''}
        readOnly={!isEditing}
        type='text' />
      : <div
        className='cell-div noselect'
        style={divStyle}>
        {renderCell(column, data)}
      </div>
    return (
      <div
        className='editable-cell small'
        role='button'
        title={!isEditing && invalidData
          ? invalidData.reason
          : undefined
        }
        style={cellStyle}
        tabIndex={0}
        onClick={this.handleClick}
        onKeyDown={this._onOuterKeyDown}>
        {cellHtml}
      </div>
    )
  }
}
<|endoftext|>


<commit_before>export { Base };

/**
 *
 */
class Base {

//   constructor() {
//   }

  /**
   * @param {number} byteLength
   *   The underlyingArrayBuffer will be large enough to contain so many bytes.
   */
  ensureArrayBuffer_byByte( byteLength ) {
    if ( ( !this.underlyingArrayBuffer ) || ( this.underlyingArrayBuffer.byteLength < byteLength ) ) {
      this.underlyingArrayBuffer = new ArrayBuffer( byteLength );

    // Otherwise, this.underlyingArrayBuffer already has the enough space. Use it directly.
    }
  }

  /**
   * @param {number} elementCount
   *   The underlyingArrayBuffer will be large enough to contain so many (Float32) elements.
   */
  ensureArrayBuffer_byFloat32( elementCount ) {
    this.ensureArrayBuffer_byByte( elementCount * Float32Array.BYTES_PER_ELEMENT );
  }

  /**
   * @param {number} elementCount
   *   Prepare this.weightsFloat32Array[] (which uses the underlyingArrayBuffer) so that its length equals elementCount.
   */
  prepare_weightsFloat32Array( elementCount ) {
    if ( ( !this.weightsFloat32Array ) || ( this.weightsFloat32Array.length != elementCount ) ) {
      this.ensureArrayBuffer_byFloat32( elementCount );
      this.weightsFloat32Array = new Float32Array( this.underlyingArrayBuffer, 0, elementCount );

    // Otherwise, this.weightsFloat32Array already has the required length. Use it directly.
    }
  }

  /**
   * Concatenate multiple number arrays into one Float32Array (this.weightsFloat32Array) in a specified order.
   *
   * It fills two data members of this object { weightsFloat32Array, weightsByteOffsetBegin }.
   *   - this.weightsFloat32Array: A Float32Array which is the concatenated result of the numberArrayArray.
   *   - this.weightsByteOffsetBegin: A random offset byte count inside weightsFloat32Array.
   *
   * Note: A re-usable ArrayBuffer will be used as the underlying ArrayBuffer of the this.weightsFloat32Array.
   *       The this.weightsFloat32Array itself may also be re-used. So caller should not modify it.
   *
   *
   * @param {string[]} nameOrderArray
   *   An array of string. Every element is a string name of a parameter. The number array in nameNumberArrayObject will be concatenated
   * according to the element order of this nameOrderArray[].
   *
   * @param {object} nameNumberArrayObject
   *   An object whose all properties are number or number array. It is a map from a string name to a number or number array. The names
   * should be found in nameOrderArray[].
   *
   * @param {number} weightsElementOffsetBegin
   *   Offset how many elements (4 bytes per element) at the beginning of the result weightsFloat32Array.
   *
   * @returned {number} weightsElementOffsetBegin
   */
  setByConcat( nameOrderArray, nameNumberArrayObject, weightsElementOffsetBegin = 0 ) {

    this.weightsByteOffsetBegin = weightsElementOffsetBegin * Float32Array.BYTES_PER_ELEMENT; // Skip the un-used byte count.

    // Calculate the total length include the extra offset.
    let weightsTotalLength = weightsElementOffsetBegin;
    for ( let i = 0; i < nameOrderArray.length; ++i ) {
      let name = nameOrderArray[ i ];
      let number_or_numberArray = nameNumberArrayObject[ name ];
      if ( number_or_numberArray != undefined ) {
        if ( number_or_numberArray instanceof Array ) {
          weightsTotalLength += number_or_numberArray.length; // Assume number array.
        } else {
          weightsTotalLength += 1; // Assume number.
        }
      }
    }

    // Concatenate all number array into a (re-used) Float32Array.
    this.prepare_weightsFloat32Array( weightsTotalLength );
    {
      for ( let i = 0; i < this.weightsElementOffsetBegin; ++i ) { // Make-up the un-used weight values.
        this.weightsFloat32Array[ i ] = -i;
      }

      let offset = weightsElementOffsetBegin;
      for ( let i = 0; i < nameOrderArray.length; ++i ) { // Concatenate all number array into a Float32Array.
      let name = nameOrderArray[ i ];
      let number_or_numberArray = nameNumberArrayObject[ name ];

      if ( number_or_numberArray != undefined ) {
        if ( number_or_numberArray instanceof Array ) {
          this.weightsFloat32Array.set( number_or_numberArray, offset ); // Assume number array.
          offset += number_or_numberArray.length;
        } else {
          this.weightsFloat32Array[ offset ] = number_or_numberArray; // Assume number.
          offset += 1;
        }
      }
    }

  }

}
<commit_msg>Update NameNumberArrayObject_To_Float32Array.js<commit_after>export { Base };

/**
 *
 */
class Base {

//   constructor() {
//   }

  /**
   * @param {number} byteLength
   *   The underlyingArrayBuffer will be large enough to contain so many bytes.
   */
  ensureArrayBuffer_byByte( byteLength ) {
    if ( ( !this.underlyingArrayBuffer ) || ( this.underlyingArrayBuffer.byteLength < byteLength ) ) {
      this.underlyingArrayBuffer = new ArrayBuffer( byteLength );

    // Otherwise, this.underlyingArrayBuffer already has the enough space. Use it directly.
    }
  }

  /**
   * @param {number} elementCount
   *   The underlyingArrayBuffer will be large enough to contain so many (Float32) elements.
   */
  ensureArrayBuffer_byFloat32( elementCount ) {
    this.ensureArrayBuffer_byByte( elementCount * Float32Array.BYTES_PER_ELEMENT );
  }

  /**
   * @param {number} elementCount
   *   Prepare this.weightsFloat32Array[] (which uses the underlyingArrayBuffer) so that its length equals elementCount.
   */
  prepare_weightsFloat32Array( elementCount ) {
    if ( ( !this.weightsFloat32Array ) || ( this.weightsFloat32Array.length != elementCount ) ) {
      this.ensureArrayBuffer_byFloat32( elementCount );
      this.weightsFloat32Array = new Float32Array( this.underlyingArrayBuffer, 0, elementCount );

    // Otherwise, this.weightsFloat32Array already has the required length. Use it directly.
    }
  }

  /**
   * Concatenate multiple number arrays into one Float32Array (this.weightsFloat32Array) in a specified order.
   *
   * It fills two data members of this object { weightsFloat32Array, weightsByteOffsetBegin }.
   *   - this.weightsFloat32Array: A Float32Array which is the concatenated result of the numberArrayArray.
   *   - this.weightsByteOffsetBegin: A random offset byte count inside weightsFloat32Array.
   *
   * Note: A re-usable ArrayBuffer will be used as the underlying ArrayBuffer of the this.weightsFloat32Array.
   *       The this.weightsFloat32Array itself may also be re-used. So caller should not modify it.
   *
   *
   * @param {string[]} nameOrderArray
   *   An array of string. Every element is a string name of a parameter. The number array in nameNumberArrayObject will be concatenated
   * according to the element order of this nameOrderArray[].
   *
   * @param {object} nameNumberArrayObject
   *   An object whose all properties are number or number array. It is a map from a string name to a number or number array. The names
   * should be found in nameOrderArray[].
   *
   * @param {number} weightsElementOffsetBegin
   *   Offset how many elements (4 bytes per element) at the beginning of the result weightsFloat32Array.
   *
   * @returned {number} weightsElementOffsetBegin
   */
  setByConcat( nameOrderArray, nameNumberArrayObject, weightsElementOffsetBegin = 0 ) {

    this.weightsByteOffsetBegin = weightsElementOffsetBegin * Float32Array.BYTES_PER_ELEMENT; // Skip the un-used byte count.

    // Calculate the total length include the extra offset.
    let weightsTotalLength = weightsElementOffsetBegin;
    for ( let i = 0; i < nameOrderArray.length; ++i ) {
      let name = nameOrderArray[ i ];
      let number_or_numberArray = nameNumberArrayObject[ name ];
      if ( number_or_numberArray != undefined ) {
        if ( number_or_numberArray instanceof Array ) {
          weightsTotalLength += number_or_numberArray.length; // Assume number array.
        } else {
          weightsTotalLength += 1; // Assume number.
        }
      }
    }

    // Concatenate all number array into a (re-used) Float32Array.
    this.prepare_weightsFloat32Array( weightsTotalLength );
    {
      for ( let i = 0; i < this.weightsElementOffsetBegin; ++i ) { // Make-up the un-used weight values.
        this.weightsFloat32Array[ i ] = -i;
      }

      let offset = weightsElementOffsetBegin;
      for ( let i = 0; i < nameOrderArray.length; ++i ) { // Concatenate all number array into a Float32Array.
        let name = nameOrderArray[ i ];
        let number_or_numberArray = nameNumberArrayObject[ name ];

        if ( number_or_numberArray != undefined ) {
          if ( number_or_numberArray instanceof Array ) {
            this.weightsFloat32Array.set( number_or_numberArray, offset ); // Assume number array.
            offset += number_or_numberArray.length;
          } else {
            this.weightsFloat32Array[ offset ] = number_or_numberArray; // Assume number.
            offset += 1;
          }
        }
      }
    }
  }

}
<|endoftext|>


<commit_before>/**
 * @namespace BigOFunctions
 * @description
 * Concepts:
 *
 * - Drop constants
 *  - O(2N) = O(N)
 * - Drop non-dominant terms
 *  - O(N^2 + N) = O(N^2)
 *  - O(N + log N) = O(N)
 *  - 0(5*2^N + 1000N^100 ) = O(2^N)
 * - Multi-part algorithms
 *  - Add
 *      - A chunks of work then B chunks of work is O(A + B)
 *  - Multiply
 *      - B chunks of work every A element is O(A * B)
 *  - Base of log doesn't matter;
 *  - The base of an expoent matter: 2^N is diferent from 8^N which is (2^3)^N
 *
 * - Common Patterns
 *  - An ArrayList after inserting N elements it needs to resize its internal array, but it's so rare that the time is amortized
 *  - When the number of elements get halved each time, that will likely be a O(log N) runtime. eg. balanced binary search
 *  - When a recursive function that makes multiple calls, the runtime will often look like O(branches^depth), where branches is the number of times each recursive call branches.
 *  - O(2^log N) = O(N)
 *
 * - Examples
 *  - O(N + P), where P < N/2 = O(N) // Drop non-dominant
 *  - O(2N) = O(N) // Drop constants
 *  - O(N + log N) = O(N) // Drop non-dominant
 *  - O(N + M) = O(N + M) // It's not possible to determine the relation between M and N
 **/

/**
 * Function that calculates the powers of 2.
 * Big O: O(log N)
 *
 * @memberOf BigOFunctions
 * @param {number} n Value of the exponent.
 * @returns Calculation result.
 */
function powers0f2(n) {
    this.step('n');
    if (n < 1) {
        return 0;
    } else if (n == 1) {
        return 1;
    } else {
        let prev = powers0f2.apply(this, [n / 2]); // every recursive call does N/2, so log N
        let curr = prev * 2;
        return curr;
    }
}

/**
 * Simple function that executes only one statement.
 * Big O: O(1)
 *
 * @memberOf BigOFunctions
 * @param {number} n First number
 * @param {number} m Second number
 * @returns Multipliation of n by m
 */
function O1Simple(n, m) {
    this.step('n', 'm');
    return n * m; // 1 work
}

/**
 * O(N) example that executes one statement per array item.
 * Big O: O(N)
 *
 * @memberOf BigOFunctions
 * @param {array} array to loop trought.
 */
function ONSimple(array) {
    array.forEach(() => this.step('array')); // N work
}

/**
 * Simple function that executes one statement in a m * n combination.
 * Big O: O(N^2)
 *
 * @memberOf BigOFunctions
 * @param {array} array to loop trought.
 */
function ON2Simple(array) {
    array.forEach(() => { // N work
        array.forEach(() => this.step('array')); // within N work
    });
}

/**
 * Simple function that executes one statement every 2 * n combination.
 * Big O: O(2^N)
 *
 * @memberOf BigOFunctions
 * @param {number} n Number of times to duplicate execution.
 */
function O2NSimple(n) {
    this.step('n');
    if (n <= 1) {
        return 1;
    }
    O2NSimple.apply(this, [n - 1]) + O2NSimple.apply(this, [n - 1]); // branches^N = 2^N work
}

/**
 * Function that calculates a fibonacci number.
 * Big O: O(2^N)
 *
 * @memberOf BigOFunctions
 * @param {number} n Number to calculate fibonacci.
 */
function fibonacci(n) {
    this.step('n');
    if (n <= 0) {
        return 0;
    } else if (n == 1) {
        return 1;
    }
    return fibonacci.apply(this, [n - 1]) + fibonacci.apply(this, [n - 2]); // branches^N = 2^N work
}

/**
 * O(A + B) example that has two sequencial array loops.
 * Big O: O(A + B)
 *
 * @memberOf BigOFunctions
 * @param {array} arrayA First array.
 * @param {array} arrayB Second array.
 */
function OADBSimple(arrayA, arrayB) {
    arrayA.forEach(() => {
        this.step('arrayA'); // A work
    });

    arrayB.forEach(() => {
        this.step('arrayB'); // B work
    });
}

/**
 * O(A * B) example that has an array loop inside other array loop.
 * Big O: O(A * B)
 *
 * @memberOf BigOFunctions
 * @param {array} arrayA First array.
 * @param {array} arrayB Second array.
 */
function ONXNSimple(arrayA, arrayB) {
    arrayA.forEach(() => { // A work
        this.step('arrayA');
        arrayB.forEach(() => { // B work
            this.step('arrayB');
        });
    });
}

/**
 * Simple function that has an array loop inside other array loop. The inner loop does half of the work of the outer loop
 * Big O: O(N * N/2) = O((N^2)/2) = O(N^2)
 *
 * @memberOf BigOFunctions
 * @param {array} array array.
 */
function ON2HalfInnerLoop(array) {
    for (let i = 0; i < array.length; i++) { // N work
        for (let j = i + 1; j < array.length; j++) { // N/2 work (on average)
        }
    }
}

/**
 * O(N) example that has an array loop, the loop is array size / 2.
 * Big O: O(N)
 *
 * @memberOf BigOFunctions
 * @param {array} array array.
 */
function reverse(array) {
    for (let i = 0; i < array.length / 2; i++) { // It does N even though is array / 2. array / 2 is the N work.
        let other = array.length - i - 1;
        let temp = array[i];
        array[i] = array[other];
        array[other] = temp;
    }
}

/**
 * 2^log N example.
 * Big O: O(2^log N) = O(N)
 *
 * @memberOf BigOFunctions
 * @param {object} node object which is a balanced binary tree.
 * @returns sum of all nodes.
 */
function sum(node) {
    if (node == null) {
        return 0;
    }
    return sum(node.left) + node.value + sum(node.right); // 2^log N work = O(N) which 2^depth but each recursive call does log N work.
}

module.exports = {
    powers0f2,
    O1Simple,
    ONSimple,
    ON2Simple,
    O2NSimple,
    fibonacci,
    OADBSimple,
    ONXNSimple,
    ON2HalfInnerLoop
};<commit_msg>Adds more big o examples.<commit_after>/**
 * @namespace BigOFunctions
 * @description
 * Concepts:
 *
 * - Drop constants
 *  - O(2N) = O(N)
 * - Drop non-dominant terms
 *  - O(N^2 + N) = O(N^2)
 *  - O(N + log N) = O(N)
 *  - 0(5*2^N + 1000N^100 ) = O(2^N)
 * - Multi-part algorithms
 *  - Add
 *      - A chunks of work then B chunks of work is O(A + B)
 *  - Multiply
 *      - B chunks of work every A element is O(A * B)
 *  - Base of log doesn't matter;
 *  - The base of an expoent matter: 2^N is diferent from 8^N which is (2^3)^N
 *
 * - Common Patterns
 *  - An ArrayList after inserting N elements it needs to resize its internal array, but it's so rare that the time is amortized
 *  - When the number of elements get halved each time, that will likely be a O(log N) runtime. eg. balanced binary search
 *  - When a recursive function that makes multiple calls, the runtime will often look like O(branches^depth), where branches is the number of times each recursive call branches.
 *  - O(2^log N) = O(N)
 *
 * - Examples
 *  - O(N + P), where P < N/2 = O(N) // Drop non-dominant
 *  - O(2N) = O(N) // Drop constants
 *  - O(N + log N) = O(N) // Drop non-dominant
 *  - O(N + M) = O(N + M) // It's not possible to determine the relation between M and N
 **/

/**
 * Simple function that executes only one statement.
 * Big O: O(1)
 *
 * @memberOf BigOFunctions
 * @param {number} n First number
 * @param {number} m Second number
 * @returns Multipliation of n by m
 */
function O1Simple(n, m) {
    this.step('n', 'm');
    return n * m; // 1 work
}

/**
 * Function that calculates the powers of 2.
 * Big O: O(log N)
 *
 * @memberOf BigOFunctions
 * @param {number} n Value of the exponent.
 * @returns Calculation result.
 */
function powers0f2(n) {
    this.step('n');
    if (n < 1) {
        return 0;
    } else if (n == 1) {
        return 1;
    } else {
        let prev = powers0f2.apply(this, [n / 2]); // every recursive call does N/2, so log N
        let curr = prev * 2;
        return curr;
    }
}

/**
 * Function that checks if n is a prime number. It only needs to go up to the square root of N.
 * Big O: O(vN)
 *
 * @memberOf BigOFunctions
 * @param {number} n Number to check.
 * @returns {boolean} If it is or not.
 */
function isPrime(n) {
    for (let x = 2; x <= Math.sqrt(n); x++) {
        if (n % x == 0) {
            return false;
        }
    }
    return true;
}

/**
 * O(N) example that executes one statement per array item.
 * Big O: O(N)
 *
 * @memberOf BigOFunctions
 * @param {array} array to loop trought.
 */
function ONSimple(array) {
    array.forEach(() => this.step('array')); // N work
}

/**
 * O(N) example function with 2 consecutives loops that executes one statement per array item.
 * Big O: O(N+N) = O(N)
 *
 * @memberOf BigOFunctions
 * @param {array} array to loop trought.
 */
function ONAdd(array) {
    array.forEach(() => this.step('array')); // N work
    array.forEach(() => this.step('array')); // then N work
}

/**
 * Function that calculates the factorial for number N.
 * Big O: O(N)
 *
 * @memberOf BigOFunctions
 * @param {number} n Number to calcule factorial.
 * @returns {number} Factorial calculated.
 */
function factorial(n) {
    if (n < 0) {
        return -1;
    } else if (n == 0) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

/**
     * O(N^2) example that executes one statement in a m * n combination.
 * Big O: O(N^2)
 *
 * @memberOf BigOFunctions
 * @param {array} array to loop trought.
 */
function ON2Simple(array) {
    array.forEach(() => { // N work
        array.forEach(() => this.step('array')); // within N work
    });
}

/**
 * O(N^2) example that has an array loop inside other array loop. The inner loop does half of the work of the outer loop
 * Big O: O(N * N/2) = O((N^2)/2) = O(N^2)
 *
 * @memberOf BigOFunctions
 * @param {array} array array.
 */
function ON2HalfInnerLoop(array) {
    for (let i = 0; i < array.length; i++) { // N work
        for (let j = i + 1; j < array.length; j++) { // N/2 work (on average)
        }
    }
}

/**
 * O(2^N) example that executes one statement every 2 * n combination.
 * Big O: O(2^N)
 *
 * @memberOf BigOFunctions
 * @param {number} n Number of times to duplicate execution.
 */
function O2NSimple(n) {
    this.step('n');
    if (n <= 1) {
        return 1;
    }
    O2NSimple.apply(this, [n - 1]) + O2NSimple.apply(this, [n - 1]); // branches^N = 2^N work
}

/**
 * Prints all fibonacci numbers from 0 to N.
 * Big O: O(N) * O(2^N) = O(2^N)
 *
 * @memberOf BigOFunctions
 * @param {number} n Upper number.
 */
function allFibonacci(n) {
    for (let i = 0; i < n; i++) {
        console.log(i + ': ' + fibonacci(i));
    }
}

/**
 * O(2^N) example that calculates a fibonacci number.
 * Big O: O(2^N)
 *
 * @memberOf BigOFunctions
 * @param {number} n Number to calculate fibonacci.
 */
function fibonacci(n) {
    this.step('n');
    if (n <= 0) {
        return 0;
    } else if (n == 1) {
        return 1;
    }
    return fibonacci.apply(this, [n - 1]) + fibonacci.apply(this, [n - 2]); // branches^N = 2^N work
}

/**
 * O(A + B) example that has two sequencial array loops.
 * Big O: O(A + B)
 *
 * @memberOf BigOFunctions
 * @param {array} arrayA First array.
 * @param {array} arrayB Second array.
 */
function OADBSimple(arrayA, arrayB) {
    arrayA.forEach(() => {
        this.step('arrayA'); // A work
    });

    arrayB.forEach(() => {
        this.step('arrayB'); // B work
    });
}

/**
 * O(A * B) example that has an array loop inside other array loop.
 * Big O: O(A * B)
 *
 * @memberOf BigOFunctions
 * @param {array} arrayA First array.
 * @param {array} arrayB Second array.
 */
function ONXNSimple(arrayA, arrayB) {
    arrayA.forEach(() => { // A work
        this.step('arrayA');
        arrayB.forEach(() => { // B work
            this.step('arrayB');
        });
    });
}

/**
 * O(N) example that has an array loop, the loop is array size / 2.
 * Big O: O(N)
 *
 * @memberOf BigOFunctions
 * @param {array} array array.
 */
function reverse(array) {
    for (let i = 0; i < array.length / 2; i++) { // It does N even though is array / 2. array / 2 is the N work.
        let other = array.length - i - 1;
        let temp = array[i];
        array[i] = array[other];
        array[other] = temp;
    }
}

/**
 * 2^log N example.
 * Big O: O(2^log N) = O(N)
 *
 * @memberOf BigOFunctions
 * @param {object} node object which is a balanced binary tree.
 * @returns sum of all nodes.
 */
function sum(node) {
    if (node == null) {
        return 0;
    }
    return sum(node.left) + node.value + sum(node.right); // 2^log N work = O(N) which 2^depth but each recursive call does log N work.
}

/**
 * Prints all permutations for a given string.
 * Big O: O(N * N * N!) = O(N^2 * N!)
 *
 * @memberOf BigOFunctions
 * @param {any} str String to permutate
 * @param {string} [prefix=''] Substring to permutate (internal).
 */
function permutation(str, prefix = '') { // O(N!)
    if (str.length() == 0) {
        console.log(prefix); // O(1)
    } else {
        for (let i = 0; i < str.length(); i++) { // O(N)
            let rem = str.substring(0, i) + str.substring(i + 1);
            permutation(rem, prefix + str.charAt(i)); // O(N)
        }
    }
}

module.exports = {
    powers0f2,
    O1Simple,
    ONSimple,
    ON2Simple,
    O2NSimple,
    fibonacci,
    OADBSimple,
    ONXNSimple,
    ON2HalfInnerLoop
};<|endoftext|>


<commit_before>const _ = require('lodash');
const Promise = require('bluebird');
const moment = require('moment-timezone');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const logging = require('@tryghost/logging');
const models = require('../../models');
const MailgunClient = require('@tryghost/mailgun-client');
const sentry = require('../../../shared/sentry');
const debug = require('@tryghost/debug')('mega');
const postEmailSerializer = require('../mega/post-email-serializer');
const configService = require('../../../shared/config');
const settingsCache = require('../../../shared/settings-cache');

const messages = {
    error: 'The email service was unable to send an email batch.'
};

const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});

/**
 * An object representing batch request result
 * @typedef { Object } BatchResultBase
 * @property { string } data - data that is returned from Mailgun or one which Mailgun was called with
 */
class BatchResultBase {
    constructor(id) {
        this.id = id;
    }
}

class SuccessfulBatch extends BatchResultBase { }

class FailedBatch extends BatchResultBase {
    constructor(id, error) {
        super(...arguments);
        error.originalMessage = error.message;

        if (error.statusCode >= 500) {
            error.message = 'Email service is currently unavailable - please try again';
        } else if (error.statusCode === 401) {
            error.message = 'Email failed to send - please verify your credentials';
        } else if (error.message && error.message.toLowerCase().includes('dmarc')) {
            error.message = 'Unable to send email from domains implementing strict DMARC policies';
        } else if (error.message.includes(`'to' parameter is not a valid address`)) {
            error.message = 'Recipient is not a valid address';
        } else {
            error.message = `Email failed to send "${error.originalMessage}" - please verify your email settings`;
        }

        this.error = error;
    }
}

/**
 * An email address
 * @typedef { string } EmailAddress
 */

/**
 * An object representing an email to send
 * @typedef { Object } Email
 * @property { string } html - The html content of the email
 * @property { string } subject - The subject of the email
 */

module.exports = {
    BATCH_SIZE: MailgunClient.BATCH_SIZE,
    SuccessfulBatch,
    FailedBatch,

    // accepts an ID rather than an Email model to better support running via a job queue
    async processEmail({emailId, options}) {
        const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
        const emailModel = await models.Email.findOne({id: emailId}, knexOptions);

        if (!emailModel) {
            throw new errors.IncorrectUsageError({
                message: 'Provided email id does not match a known email record',
                context: {
                    id: emailId
                }
            });
        }

        if (emailModel.get('status') !== 'pending') {
            throw new errors.IncorrectUsageError({
                message: 'Emails can only be processed when in the "pending" state',
                context: `Email "${emailId}" has state "${emailModel.get('status')}"`,
                code: 'EMAIL_NOT_PENDING'
            });
        }

        await emailModel.save({status: 'submitting'}, Object.assign({}, knexOptions, {patch: true}));

        // get batch IDs via knex to avoid model instantiation
        // only fetch pending or failed batches to avoid re-sending previously sent emails
        const batchIds = await models.EmailBatch
            .getFilteredCollectionQuery({filter: `email_id:${emailId}+status:[pending,failed]`}, knexOptions)
            .select('id', 'member_segment');

        const batchResults = await Promise.map(batchIds, async ({id: emailBatchId, member_segment: memberSegment}) => {
            try {
                await this.processEmailBatch({emailBatchId, options, memberSegment});
                return new SuccessfulBatch(emailBatchId);
            } catch (error) {
                return new FailedBatch(emailBatchId, error);
            }
        }, {concurrency: 10});

        const successes = batchResults.filter(response => (response instanceof SuccessfulBatch));
        const failures = batchResults.filter(response => (response instanceof FailedBatch));
        const emailStatus = failures.length ? 'failed' : 'submitted';

        let error;

        if (failures.length) {
            error = failures[0].error.message;
        }

        if (error && error.length > 2000) {
            error = error.substring(0, 2000);
        }

        try {
            await models.Email.edit({
                status: emailStatus,
                results: JSON.stringify(successes),
                error: error,
                error_data: JSON.stringify(failures) // NOTE: need to discuss how we store this
            }, {
                id: emailModel.id
            });
        } catch (err) {
            sentry.captureException(err);
            logging.error(err);
        }

        return batchResults;
    },

    // accepts an ID rather than an EmailBatch model to better support running via a job queue
    async processEmailBatch({emailBatchId, options, memberSegment}) {
        const knexOptions = _.pick(options, ['transacting', 'forUpdate']);

        const emailBatchModel = await models.EmailBatch
            .findOne({id: emailBatchId}, Object.assign({}, knexOptions, {withRelated: 'email'}));

        if (!emailBatchModel) {
            throw new errors.IncorrectUsageError({
                message: 'Provided email_batch id does not match a known email_batch record',
                context: {
                    id: emailBatchId
                }
            });
        }

        if (!['pending','failed'].includes(emailBatchModel.get('status'))) {
            throw new errors.IncorrectUsageError({
                message: 'Email batches can only be processed when in the "pending" or "failed" state',
                context: `Email batch "${emailBatchId}" has state "${emailBatchModel.get('status')}"`
            });
        }

        // get recipient rows via knex to avoid costly bookshelf model instantiation
        const recipientRows = await models.EmailRecipient
            .getFilteredCollectionQuery({filter: `batch_id:${emailBatchId}`});

        await emailBatchModel.save({status: 'submitting'}, knexOptions);

        try {
            // Load newsletter data on email
            await emailBatchModel.relations.email.getLazyRelation('newsletter', {require: false, ...knexOptions});

            // Load post data on email - for content gating on paywall
            await emailBatchModel.relations.email.getLazyRelation('post', {require: false, ...knexOptions});

            // send the email
            const sendResponse = await this.send(emailBatchModel.relations.email.toJSON(), recipientRows, memberSegment);

            // update batch success status
            return await emailBatchModel.save({
                status: 'submitted',
                provider_id: sendResponse.id.trim().replace(/^<|>$/g, '')
            }, Object.assign({}, knexOptions, {patch: true}));
        } catch (error) {
            // update batch failed status
            await emailBatchModel.save({status: 'failed'}, knexOptions);

            // log any error that didn't come from the provider which would have already logged it
            if (!error.code || error.code !== 'BULK_EMAIL_SEND_FAILED') {
                let ghostError = new errors.InternalServerError({
                    err: error
                });
                sentry.captureException(ghostError);
                logging.error(ghostError);
                throw ghostError;
            }

            throw error;
        } finally {
            // update all email recipients with a processed_at
            await models.EmailRecipient
                .where({batch_id: emailBatchId})
                .save({processed_at: moment()}, Object.assign({}, knexOptions, {autoRefresh: false, patch: true}));
        }
    },

    /**
     * @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
     * @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
     * @param {string?} memberSegment - The member segment of the recipients
     * @returns {Promise<Object>} - {providerId: 'xxx'}
     */
    async send(emailData, recipients, memberSegment) {
        const mailgunConfigured = mailgunClient.isConfigured();
        if (!mailgunConfigured) {
            logging.warn('Bulk email has not been configured');
            return;
        }

        const startTime = Date.now();
        debug(`sending message to ${recipients.length} recipients`);

        const replacements = postEmailSerializer.parseReplacements(emailData);

        // collate static and dynamic data for each recipient ready for provider
        const recipientData = {};
        const newsletterUuid = emailData.newsletter ? emailData.newsletter.uuid : null;
        recipients.forEach((recipient) => {
            // static data for every recipient
            const data = {
                unique_id: recipient.member_uuid,
                unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(recipient.member_uuid, {newsletterUuid})
            };

            // computed properties on recipients - TODO: better way of handling these
            recipient.member_first_name = (recipient.member_name || '').split(' ')[0];

            // dynamic data from replacements
            replacements.forEach(({id, recipientProperty, fallback}) => {
                data[id] = recipient[recipientProperty] || fallback || '';
            });

            recipientData[recipient.member_email] = data;
        });

        emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);

        try {
            const response = await mailgunClient.send(emailData, recipientData, replacements);
            debug(`sent message (${Date.now() - startTime}ms)`);
            return response;
        } catch (error) {
            // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
            let ghostError = new errors.EmailError({
                err: error,
                context: tpl(messages.error),
                code: 'BULK_EMAIL_SEND_FAILED'
            });

            sentry.captureException(ghostError);
            logging.warn(ghostError);

            debug(`failed to send message (${Date.now() - startTime}ms)`);
            throw ghostError;
        }
    },

    // NOTE: for testing only!
    _mailgunClient: mailgunClient
};
<commit_msg>Improved handling of mailgun errors on bulk send (#15445)<commit_after>const _ = require('lodash');
const Promise = require('bluebird');
const moment = require('moment-timezone');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const logging = require('@tryghost/logging');
const models = require('../../models');
const MailgunClient = require('@tryghost/mailgun-client');
const sentry = require('../../../shared/sentry');
const debug = require('@tryghost/debug')('mega');
const postEmailSerializer = require('../mega/post-email-serializer');
const configService = require('../../../shared/config');
const settingsCache = require('../../../shared/settings-cache');

const messages = {
    error: 'The email service received an error from mailgun and was unable to send.'
};

const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});

/**
 * An object representing batch request result
 * @typedef { Object } BatchResultBase
 * @property { string } data - data that is returned from Mailgun or one which Mailgun was called with
 */
class BatchResultBase {
    constructor(id) {
        this.id = id;
    }
}

class SuccessfulBatch extends BatchResultBase { }

class FailedBatch extends BatchResultBase {
    constructor(id, error) {
        super(...arguments);
        error.originalMessage = error.message;

        if (error.statusCode >= 500) {
            error.message = 'Email service is currently unavailable - please try again';
        } else if (error.statusCode === 401) {
            error.message = 'Email failed to send - please verify your credentials';
        } else if (error.message && error.message.toLowerCase().includes('dmarc')) {
            error.message = 'Unable to send email from domains implementing strict DMARC policies';
        } else if (error.message.includes(`'to' parameter is not a valid address`)) {
            error.message = 'Recipient is not a valid address';
        } else {
            error.message = `Email failed to send "${error.originalMessage}" - please verify your email settings`;
        }

        this.error = error;
    }
}

/**
 * An email address
 * @typedef { string } EmailAddress
 */

/**
 * An object representing an email to send
 * @typedef { Object } Email
 * @property { string } html - The html content of the email
 * @property { string } subject - The subject of the email
 */

module.exports = {
    BATCH_SIZE: MailgunClient.BATCH_SIZE,
    SuccessfulBatch,
    FailedBatch,

    // accepts an ID rather than an Email model to better support running via a job queue
    async processEmail({emailId, options}) {
        const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
        const emailModel = await models.Email.findOne({id: emailId}, knexOptions);

        if (!emailModel) {
            throw new errors.IncorrectUsageError({
                message: 'Provided email id does not match a known email record',
                context: {
                    id: emailId
                }
            });
        }

        if (emailModel.get('status') !== 'pending') {
            throw new errors.IncorrectUsageError({
                message: 'Emails can only be processed when in the "pending" state',
                context: `Email "${emailId}" has state "${emailModel.get('status')}"`,
                code: 'EMAIL_NOT_PENDING'
            });
        }

        await emailModel.save({status: 'submitting'}, Object.assign({}, knexOptions, {patch: true}));

        // get batch IDs via knex to avoid model instantiation
        // only fetch pending or failed batches to avoid re-sending previously sent emails
        const batchIds = await models.EmailBatch
            .getFilteredCollectionQuery({filter: `email_id:${emailId}+status:[pending,failed]`}, knexOptions)
            .select('id', 'member_segment');

        const batchResults = await Promise.map(batchIds, async ({id: emailBatchId, member_segment: memberSegment}) => {
            try {
                await this.processEmailBatch({emailBatchId, options, memberSegment});
                return new SuccessfulBatch(emailBatchId);
            } catch (error) {
                return new FailedBatch(emailBatchId, error);
            }
        }, {concurrency: 10});

        const successes = batchResults.filter(response => (response instanceof SuccessfulBatch));
        const failures = batchResults.filter(response => (response instanceof FailedBatch));
        const emailStatus = failures.length ? 'failed' : 'submitted';

        let error;

        if (failures.length) {
            error = failures[0].error.message;
        }

        if (error && error.length > 2000) {
            error = error.substring(0, 2000);
        }

        try {
            await models.Email.edit({
                status: emailStatus,
                results: JSON.stringify(successes),
                error: error,
                error_data: JSON.stringify(failures) // NOTE: need to discuss how we store this
            }, {
                id: emailModel.id
            });
        } catch (err) {
            sentry.captureException(err);
            logging.error(err);
        }

        return batchResults;
    },

    // accepts an ID rather than an EmailBatch model to better support running via a job queue
    async processEmailBatch({emailBatchId, options, memberSegment}) {
        const knexOptions = _.pick(options, ['transacting', 'forUpdate']);

        const emailBatchModel = await models.EmailBatch
            .findOne({id: emailBatchId}, Object.assign({}, knexOptions, {withRelated: 'email'}));

        if (!emailBatchModel) {
            throw new errors.IncorrectUsageError({
                message: 'Provided email_batch id does not match a known email_batch record',
                context: {
                    id: emailBatchId
                }
            });
        }

        if (!['pending','failed'].includes(emailBatchModel.get('status'))) {
            throw new errors.IncorrectUsageError({
                message: 'Email batches can only be processed when in the "pending" or "failed" state',
                context: `Email batch "${emailBatchId}" has state "${emailBatchModel.get('status')}"`
            });
        }

        // get recipient rows via knex to avoid costly bookshelf model instantiation
        const recipientRows = await models.EmailRecipient
            .getFilteredCollectionQuery({filter: `batch_id:${emailBatchId}`});

        await emailBatchModel.save({status: 'submitting'}, knexOptions);

        try {
            // Load newsletter data on email
            await emailBatchModel.relations.email.getLazyRelation('newsletter', {require: false, ...knexOptions});

            // Load post data on email - for content gating on paywall
            await emailBatchModel.relations.email.getLazyRelation('post', {require: false, ...knexOptions});

            // send the email
            const sendResponse = await this.send(emailBatchModel.relations.email.toJSON(), recipientRows, memberSegment);

            // update batch success status
            return await emailBatchModel.save({
                status: 'submitted',
                provider_id: sendResponse.id.trim().replace(/^<|>$/g, '')
            }, Object.assign({}, knexOptions, {patch: true}));
        } catch (error) {
            // update batch failed status
            await emailBatchModel.save({status: 'failed'}, knexOptions);

            // log any error that didn't come from the provider which would have already logged it
            if (!error.code || error.code !== 'BULK_EMAIL_SEND_FAILED') {
                let ghostError = new errors.InternalServerError({
                    err: error
                });
                sentry.captureException(ghostError);
                logging.error(ghostError);
                throw ghostError;
            }

            throw error;
        } finally {
            // update all email recipients with a processed_at
            await models.EmailRecipient
                .where({batch_id: emailBatchId})
                .save({processed_at: moment()}, Object.assign({}, knexOptions, {autoRefresh: false, patch: true}));
        }
    },

    /**
     * @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
     * @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
     * @param {string?} memberSegment - The member segment of the recipients
     * @returns {Promise<Object>} - {providerId: 'xxx'}
     */
    async send(emailData, recipients, memberSegment) {
        const mailgunConfigured = mailgunClient.isConfigured();
        if (!mailgunConfigured) {
            logging.warn('Bulk email has not been configured');
            return;
        }

        const startTime = Date.now();
        debug(`sending message to ${recipients.length} recipients`);

        const replacements = postEmailSerializer.parseReplacements(emailData);

        // collate static and dynamic data for each recipient ready for provider
        const recipientData = {};
        const newsletterUuid = emailData.newsletter ? emailData.newsletter.uuid : null;
        recipients.forEach((recipient) => {
            // static data for every recipient
            const data = {
                unique_id: recipient.member_uuid,
                unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(recipient.member_uuid, {newsletterUuid})
            };

            // computed properties on recipients - TODO: better way of handling these
            recipient.member_first_name = (recipient.member_name || '').split(' ')[0];

            // dynamic data from replacements
            replacements.forEach(({id, recipientProperty, fallback}) => {
                data[id] = recipient[recipientProperty] || fallback || '';
            });

            recipientData[recipient.member_email] = data;
        });

        emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);

        try {
            const response = await mailgunClient.send(emailData, recipientData, replacements);
            debug(`sent message (${Date.now() - startTime}ms)`);
            return response;
        } catch (err) {
            let ghostError = new errors.EmailError({
                err,
                message: tpl(messages.error),
                context: `Mailgun Error ${err.error.status}: ${err.error.details}`,
                // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
                help: `https://ghost.org/docs/newsletters/#bulk-email-configuration`,
                code: 'BULK_EMAIL_SEND_FAILED'
            });

            sentry.captureException(ghostError);
            logging.warn(ghostError);

            debug(`failed to send message (${Date.now() - startTime}ms)`);
            throw ghostError;
        }
    },

    // NOTE: for testing only!
    _mailgunClient: mailgunClient
};
<|endoftext|>


<commit_before>teamTaskManager.factory('teamDetail', ['$http', '$q',
            function ($http, $q) {
                return {
                    getCurrentUserPlayers: function (teamId, inCoachMode) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/CurrentUserPlayers', { params: { teamId: teamId, inCoachMode: inCoachMode } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    getCurrentUserId: function () {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/GetCurrentUserId', { params: { inCoachMode: inCoachMode } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    getCurrentUserTeams: function (inCoachMode) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/CurrentUserTeams', { params: { inCoachMode: inCoachMode } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    addPlayerToCurrentUser: function (playerId) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/AddPlayerToCurrentUser', { params: { playerId: playerId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    removePlayerFromCurrentUser: function (playerId) {
                        var deferred = $q.defer();
                        $http.delete('/api/teaminfo/DeletePlayerFromCurrentUser', { params: { playerId: playerId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    addTeamToCurrentUser: function (teamId) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/AddTeamToCurrentUser', { params: { teamId: teamId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    removeTeamFromCurrentUser: function (teamId) {
                        var deferred = $q.defer();
                        $http.delete('/api/teaminfo/DeleteTeamFromCurrentUser', { params: { teamId: teamId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    getCurrentUserDetails: function () {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/GetCurrentUserDetails').success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    getTaskAssignments: function (teamId) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/GetTaskAssignments', { params: { teamId: teamId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    toggleTaskForCurrentPlayer: function (gameId, taskId, playerId) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/ToggleTaskForCurrentPlayer', { params: { gameId: gameId, taskId: taskId, playerId:playerId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    }
                };
            }]);
<commit_msg>manually revert change<commit_after>teamTaskManager.factory('teamDetail', ['$http', '$q',
            function ($http, $q) {
                return {
                    getCurrentUserPlayers: function (teamId, inCoachMode) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/CurrentUserPlayers', { params: { teamId: teamId, inCoachMode: inCoachMode } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    getCurrentUserId: function () {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/GetCurrentUserId').success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    getCurrentUserTeams: function (inCoachMode) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/CurrentUserTeams', { params: { inCoachMode: inCoachMode } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    addPlayerToCurrentUser: function (playerId) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/AddPlayerToCurrentUser', { params: { playerId: playerId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    removePlayerFromCurrentUser: function (playerId) {
                        var deferred = $q.defer();
                        $http.delete('/api/teaminfo/DeletePlayerFromCurrentUser', { params: { playerId: playerId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    addTeamToCurrentUser: function (teamId) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/AddTeamToCurrentUser', { params: { teamId: teamId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    removeTeamFromCurrentUser: function (teamId) {
                        var deferred = $q.defer();
                        $http.delete('/api/teaminfo/DeleteTeamFromCurrentUser', { params: { teamId: teamId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    getCurrentUserDetails: function () {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/GetCurrentUserDetails').success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    getTaskAssignments: function (teamId) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/GetTaskAssignments', { params: { teamId: teamId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    },
                    toggleTaskForCurrentPlayer: function (gameId, taskId, playerId) {
                        var deferred = $q.defer();
                        $http.get('/api/teaminfo/ToggleTaskForCurrentPlayer', { params: { gameId: gameId, taskId: taskId, playerId:playerId } }).success(function (data) {
                            deferred.resolve(data);
                        }).error(function (error) {
                            deferred.reject();
                        });
                        return deferred.promise;
                    }
                };
            }]);
<|endoftext|>


<commit_before>/** Version: 0.7.1 **/
/**
 * Copyright 2008 Toomas Rmer
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
**/
/*
	Representation of the PGN format. Different meta information
	about the actual game(s) plus the moves and result of the game.
*/
function Pgn(pgn) {
	// properties of the game eg players, ELOs etc
	this.props = new Object();
	this.requiredProps = ['Result','Black','White','Date',
								'Round','Site','Event'];
	this.requiredLength = this.requiredProps.length;
	// the moves, one move contains the black and white move
	this.moves = new Array();
	// the current move in the game
	this.currentMove = 0;
	// for outputting white and black moves separately
	this.skip = 0;

	// strip newlines
	this.pgnOrig = pgn;
	pgn = this.normalize(pgn);

	this.pgn = pgn;
	this.pgnRaw = pgn;
	this.pgnStripped = this.stripComments(pgn);

	this.pgn = this.extractTags(this.pgn);

	this.pgn = this.stripComments(this.pgn,true);

	// the moves;
	var themoves = this.pgn.split(" ");
	var ply = new Array();
	ply[1] = null;
	var plyidx = 0;	//make this 1 if FEN and black to move
	if (this.isBlackToMove(this.props['FEN'])) {
		plyidx = 1;
		this.skip = 1;
	}

	if (themoves.length>0 && themoves[themoves.length-1] == "...") {
		 themoves = themoves.slice(0, themoves.length-1);
	}

	var sizeOfTheMoves = this.isResultIncluded(themoves) ?
							themoves.length - 1 : themoves.length;

	for (var i=0;i<sizeOfTheMoves;i++) {	//don't handle game end bit
		if (themoves[i]) {
			themoves[i] = themoves[i].trim();

			themoves[i] = this.includeOrigination(themoves[i]);
			themoves[i] = this.removeMoveNumber(themoves[i])
			if( themoves[i].length > 0 ) {
				ply[plyidx] = themoves[i];
				if (plyidx == 1) {	//black's move or last move
					var move = new Move(ply[0], ply[1]);
					this.moves[this.moves.length] = move;
					plyidx = 0;
					ply = new Array();
					ply[1] = null;
				}
				else {
					plyidx = 1;
				}
			}
		}
	}

	if (ply[0] || ply[1]) {
		var move = new Move(ply[0], ply[1]);
		this.moves[this.moves.length] = move;
	}
}
/**
 *	Checks FEN to see if it is Black to start
 */
Pgn.prototype.isBlackToMove = function(FEN) {
	return (FEN && / b | B /.test(FEN));
}
Pgn.prototype.isResultIncluded = function(themoves) {
	var gameOverregex = new Array(
		/1\/2-1\/2/,
		/0-1/,
		/1-0/,
		/\*/
	);
	var result = false;

	if (themoves.length>0) {
		for (var i=0;i<gameOverregex.length;i++) {
			result |= !!themoves[themoves.length-1].match(gameOverregex[i]);
		}
	}
	return result;
}
Pgn.prototype.includeOrigination = function(move) {
	if (this.moveHasOrigination(move)) {
		var fromTo = move.split("-");
		var newMove;
		if (fromTo[0].match(/[0-9]*?\.?([A-Z])/) != null) {
			 // we can just replace the - with nothing
			 newMove  = move.replace("-","");
		}
		else {
			var matches = fromTo[0].match(/[0-9]+\./);
			if (matches) {
				newMove = matches[0]+fromTo[1];
			}
			else {
				newMove = fromTo[1];
			}
		}
		return newMove;
	}
	return move;
}
/**
 *	If the move is prefaced by a move number, remove it.
 */
Pgn.prototype.removeMoveNumber = function(move) {
	return move.replace(/^[1-9][0-9]*\.?[ ]*/g,"")
}
/**
 *	will return the pgn string without comments or with comments replaced by dashes.
 */
Pgn.prototype.stripComments = function(pgn, replace) {
	if (this.isBroken(pgn))
		return this.stripItBroken(pgn, replace);
	else
		return this.stripIt(pgn, replace);
}
/**
 *	Does the move have information on its origin as well as its destination
 */
Pgn.prototype.moveHasOrigination = function(move) {
	return move.indexOf("-") != -1
			 && !/[0|o]-[0|o]/i.test(move)
			 && !/[0|o]-[0|o]-[0|o]/i.test(move);
}
/**
 *	This will "normalize" the pgn file by removing line breaks, collapsing strings of
 *	spaces, and replacing the symbols for moves with the characters (!, ?, etc.)
 */
Pgn.prototype.normalize = function(pgn) {
	pgn = pgn.replace(/\n/g," ");

	// replace dollar signs
	//"!", "?", "!!", "!?", "?!", and "??"
	pgn = pgn.replace(/\ \$1[0-9]*/g, "!");
	pgn = pgn.replace(/\ \$2[0-9]*/g, "?");
	pgn = pgn.replace(/\ \$3[0-9]*/g, "!!");
	pgn = pgn.replace(/\ \$4[0-9]*/g, "??");
	pgn = pgn.replace(/\ \$5[0-9]*/g, "!?");
	pgn = pgn.replace(/\ \$6[0-9]*/g, "?!");
	pgn = pgn.replace(/\ \$[0-9]+/g, "");

	// make double spaces to single spaces
	return pgn.replace(/\s+/g,' ');
}
/**
 *	This function takes a pgn game string and extracts all the tags into the class
 *	variable props as key/value pairs. it returns the pgn string, minus the tags.
 */
Pgn.prototype.extractTags = function (pgn) {
	var matches;
	var reprop = /\[([^\]]*)\]/gi;
	if (matches = pgn.match(reprop)) {
		// extract information from each matched property
		for(var i = 0;i < matches.length; i++) {
			// lose the brackets
			tmpMatches = matches[i].substring(1, matches[i].length-1);
			// split by the first space
			var key = tmpMatches.substring(0, tmpMatches.indexOf(" "));
			var value = tmpMatches.substring(tmpMatches.indexOf(" ")+1);
			if (value.charAt(0) == '"')
				value = value.substr(1);
			if (value.charAt(value.length-1) == '"')
				value = value.substr(0, value.length-1);

			this.props[key] = value;
			pgn = pgn.replace(matches[i], "");
		}
	}
	length = this.requiredLength;
	while(length--) {
		if(!this.props[this.requiredProps[length]])
			this.props[this.requiredProps[length]] = "?";
	}

	return pgn.replace(/\[[^\]]*\]/g,'').trim();
}

Pgn.prototype.nextMove = function() {
		var rtrn = null;
		try{
			if (this.skip) {
				this.skip = 0;
				rtrn = new Array(this.moves[this.currentMove].black,
													'black');
				this.currentMove++;
			}
			else {
				this.skip = 1;
				rtrn = new Array(this.moves[this.currentMove].white,
											'white');
			}

			if (rtrn[0] == null || rtrn[0].length == 0)
				rtrn = null;
			return rtrn;
		}
		catch (e) {
			return null;
		}
	};

Pgn.prototype.getComment = function(move, idx) {
		var i = this.pgnStripped.indexOf(move,idx);
		if (i == -1) {
			//throw("getComment error, could not find move '"
			//				+move+"'"+", with index '"+idx+"'");
			return [null,idx];
		}

		for (var j=i+move.length;j<this.pgnStripped.length;j++) {
			var c = this.pgnStripped.charAt(j);
			switch (c) {
				case ' ':
					break;
				case '_':	//found comment
					for (var k=j;k<this.pgnStripped.length;k++) {
						var c2 = this.pgnStripped.charAt(k);
						switch (c2) {
							case '_':	//found comment
								break;
							default:	//no comment
								// we might have many comments separated with spaces
								// as we strip all double spaces to single ones we
								// can just check for the next char being '_'
								if (this.pgnStripped.length>k+1
									 	&& this.pgnStripped.charAt(k+1) == '_') {
									continue;
								}
								return [this.pgnRaw.substring(j,k),k];
						}
					}
					break;
				default:	//no comment
					return [null,idx];
			}
		}
		return [null,idx];
	};

Pgn.prototype.isBroken = function (val) {
		var pCount = 0;
		var cCount = 0;
		var lastOne = "";
		var inComment = false;
		for (var i=0;i<val.length;i++) {
			var c = val.charAt(i);
			if(inComment) {
				switch (c) {
					case '}':
						inComment = false;
						// closing a non-existent curly brace
						if (cCount == 0)
							return true;
						// if we're closing a parenthesis instead of a curly
						if (lastOne == "p")
							return true;
						lastOne = "";
						cCount--;
						break;
					case '{':
						// Cannot nest comments
						return true;
				}
			} else {
				switch (c) {
					case '(':
						pCount++;
						lastOne = "p";
						break;
					case ')':
						// closing a non-existent curly brace
						if (pCount == 0)
							return true;
						// closing a curly instead of a parenthesis
						if (lastOne == "c")
							return true;
						lastOne = "";
						pCount--;
						break;
					case '{':
						inComment = true;
						cCount++;
						lastOne = "c";
						break;
				}
			}
		}
		return false;
	};

Pgn.prototype.stripItBroken = function (val, strip) {
	var count = 0;
	var out = new Array();
	/*
		At one point chesspastebin.com started getting cames
		with invalid PGNs, mostly in the form
		{ comment comment ( something between starting brackets}.
		As you can see, the ( is not closed.
		isOpen and isCurlyO are just for that to take normal
		guesses in that kind of situations.
	*/
	var isOpen = false;
	var isCurlyO = false;
	var curlyOpenedFst = false;
	for (var i=0;i<val.length;i++) {
		var c = val.charAt(i);
		switch (c) {
			case '(':
				if (!strip) {
					out[out.length] = '_';
				}
				count++;
				if (isOpen) {
					 	count--;
				}
				isOpen = true;
				break;
			case '{':
				isCurlyO = true;
				if (!strip) {
					out[out.length] = '_';
				}
				count++;
				if (!isOpen)
					 curlyOpenedFst=true;
				break;
			case '}':
				if (isOpen && isCurlyO && curlyOpenedFst) {
					// lets close the open (
					count--;
					isOpen = false;
				}
				isCurlyO = false;
				curlyOpenedFst = false;
				count--;
				if (!strip) {
					out[out.length] = '_';
				}
				break;
			case ')':
				if (isOpen) {
					 count--;
					 if (!strip) {
						 out[out.length] = '_';
					 }
					 isOpen = false;
				}
				break;
			case '\t':
				out[out.length] = ' ';
				break;
			default:
				if (count > 0) {
					if (!strip) {
						out[out.length] = '_';
					}
				}
				else {
					out[out.length] = c;
				}
		}
	}
	return out.join("");
};

/*
	Strip game comments from a PGN string. If second
	parameter set false true then comments will be replaced
	by an underscore.
*/
Pgn.prototype.stripIt = function (val, strip) {
	var count = 0;
	var out = new Array();
	var inComment = false;
	for (var i=0;i<val.length;i++) {
		var c = val.charAt(i);
		switch (c) {
			case '(':
				if (!strip) {
					out[out.length] = '_';
				}
				if(!inComment) {
					count++;
				}
				break;
			case '{':
				inComment = true;
				if (!strip) {
					out[out.length] = '_';
				}
				count++;
				break;
			case '}':
				count--;
				if (!strip) {
					out[out.length] = '_';
				}
				inComment = false;
				break;
			case ')':
				if(!inComment) {
					count--;
				}
				if (!strip) {
					out[out.length] = '_';
				}
				break;
			case '\t':
				out[out.length] = ' ';
				break;
			default:
				if (count > 0) {
					if (!strip) {
						out[out.length] = '_';
					}
				}
				else {
					out[out.length] = c;
				}
		}
	}
	return out.join("");
};
<commit_msg>Cleaned up detecting game over string<commit_after>/** Version: 0.7.1 **/
/**
 * Copyright 2008 Toomas Rmer
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
**/
/*
	Representation of the PGN format. Different meta information
	about the actual game(s) plus the moves and result of the game.
*/
function Pgn(pgn) {
	// properties of the game eg players, ELOs etc
	this.props = new Object();
	this.requiredProps = ['Result','Black','White','Date',
								'Round','Site','Event'];
	this.requiredLength = this.requiredProps.length;
	// the moves, one move contains the black and white move
	this.moves = new Array();
	// the current move in the game
	this.currentMove = 0;
	// for outputting white and black moves separately
	this.skip = 0;

	// strip newlines
	this.pgnOrig = pgn;
	pgn = this.normalize(pgn);

	this.pgn = pgn;
	this.pgnRaw = pgn;
	this.pgnStripped = this.stripComments(pgn);

	this.pgn = this.extractTags(this.pgn);

	this.pgn = this.stripComments(this.pgn,true);

	// the moves;
	var themoves = this.pgn.split(" ");
	var ply = new Array();
	ply[1] = null;
	var plyidx = 0;	//make this 1 if FEN and black to move
	if (this.isBlackToMove(this.props['FEN'])) {
		plyidx = 1;
		this.skip = 1;
	}

	if (themoves.length>0 && themoves[themoves.length-1] == "...") {
		 themoves = themoves.slice(0, themoves.length-1);
	}

	var sizeOfTheMoves = this.isResultIncluded(themoves) ?
							themoves.length - 1 : themoves.length;

	for (var i=0;i<sizeOfTheMoves;i++) {	//don't handle game end bit
		if (themoves[i]) {
			themoves[i] = themoves[i].trim();

			themoves[i] = this.includeOrigination(themoves[i]);
			themoves[i] = this.removeMoveNumber(themoves[i])
			if( themoves[i].length > 0 ) {
				ply[plyidx] = themoves[i];
				if (plyidx == 1) {	//black's move or last move
					var move = new Move(ply[0], ply[1]);
					this.moves[this.moves.length] = move;
					plyidx = 0;
					ply = new Array();
					ply[1] = null;
				}
				else {
					plyidx = 1;
				}
			}
		}
	}

	if (ply[0] || ply[1]) {
		var move = new Move(ply[0], ply[1]);
		this.moves[this.moves.length] = move;
	}
}
/**
 *	Checks FEN to see if it is Black to start
 */
Pgn.prototype.isBlackToMove = function(FEN) {
	return (FEN && / b | B /.test(FEN));
}
Pgn.prototype.isResultIncluded = function(themoves) {
	return !!themoves[themoves.length-1].match(/1\/2-1\/2|0-1|1-0|\*/);
}
Pgn.prototype.includeOrigination = function(move) {
	if (this.moveHasOrigination(move)) {
		var fromTo = move.split("-");
		var newMove;
		if (fromTo[0].match(/[0-9]*?\.?([A-Z])/) != null) {
			 // we can just replace the - with nothing
			 newMove  = move.replace("-","");
		}
		else {
			var matches = fromTo[0].match(/[0-9]+\./);
			if (matches) {
				newMove = matches[0]+fromTo[1];
			}
			else {
				newMove = fromTo[1];
			}
		}
		return newMove;
	}
	return move;
}
/**
 *	If the move is prefaced by a move number, remove it.
 */
Pgn.prototype.removeMoveNumber = function(move) {
	return move.replace(/^[1-9][0-9]*\.?[ ]*/g,"")
}
/**
 *	will return the pgn string without comments or with comments replaced by dashes.
 */
Pgn.prototype.stripComments = function(pgn, replace) {
	if (this.isBroken(pgn))
		return this.stripItBroken(pgn, replace);
	else
		return this.stripIt(pgn, replace);
}
/**
 *	Does the move have information on its origin as well as its destination
 */
Pgn.prototype.moveHasOrigination = function(move) {
	return move.indexOf("-") != -1
			 && !/[0|o]-[0|o]/i.test(move)
			 && !/[0|o]-[0|o]-[0|o]/i.test(move);
}
/**
 *	This will "normalize" the pgn file by removing line breaks, collapsing strings of
 *	spaces, and replacing the symbols for moves with the characters (!, ?, etc.)
 */
Pgn.prototype.normalize = function(pgn) {
	pgn = pgn.replace(/\n/g," ");

	// replace dollar signs
	//"!", "?", "!!", "!?", "?!", and "??"
	pgn = pgn.replace(/\ \$1[0-9]*/g, "!");
	pgn = pgn.replace(/\ \$2[0-9]*/g, "?");
	pgn = pgn.replace(/\ \$3[0-9]*/g, "!!");
	pgn = pgn.replace(/\ \$4[0-9]*/g, "??");
	pgn = pgn.replace(/\ \$5[0-9]*/g, "!?");
	pgn = pgn.replace(/\ \$6[0-9]*/g, "?!");
	pgn = pgn.replace(/\ \$[0-9]+/g, "");

	// make double spaces to single spaces
	return pgn.replace(/\s+/g,' ');
}
/**
 *	This function takes a pgn game string and extracts all the tags into the class
 *	variable props as key/value pairs. it returns the pgn string, minus the tags.
 */
Pgn.prototype.extractTags = function (pgn) {
	var matches;
	var reprop = /\[([^\]]*)\]/gi;
	if (matches = pgn.match(reprop)) {
		// extract information from each matched property
		for(var i = 0;i < matches.length; i++) {
			// lose the brackets
			tmpMatches = matches[i].substring(1, matches[i].length-1);
			// split by the first space
			var key = tmpMatches.substring(0, tmpMatches.indexOf(" "));
			var value = tmpMatches.substring(tmpMatches.indexOf(" ")+1);
			if (value.charAt(0) == '"')
				value = value.substr(1);
			if (value.charAt(value.length-1) == '"')
				value = value.substr(0, value.length-1);

			this.props[key] = value;
			pgn = pgn.replace(matches[i], "");
		}
	}
	length = this.requiredLength;
	while(length--) {
		if(!this.props[this.requiredProps[length]])
			this.props[this.requiredProps[length]] = "?";
	}

	return pgn.replace(/\[[^\]]*\]/g,'').trim();
}

Pgn.prototype.nextMove = function() {
		var rtrn = null;
		try{
			if (this.skip) {
				this.skip = 0;
				rtrn = new Array(this.moves[this.currentMove].black,
													'black');
				this.currentMove++;
			}
			else {
				this.skip = 1;
				rtrn = new Array(this.moves[this.currentMove].white,
											'white');
			}

			if (rtrn[0] == null || rtrn[0].length == 0)
				rtrn = null;
			return rtrn;
		}
		catch (e) {
			return null;
		}
	};

Pgn.prototype.getComment = function(move, idx) {
		var i = this.pgnStripped.indexOf(move,idx);
		if (i == -1) {
			//throw("getComment error, could not find move '"
			//				+move+"'"+", with index '"+idx+"'");
			return [null,idx];
		}

		for (var j=i+move.length;j<this.pgnStripped.length;j++) {
			var c = this.pgnStripped.charAt(j);
			switch (c) {
				case ' ':
					break;
				case '_':	//found comment
					for (var k=j;k<this.pgnStripped.length;k++) {
						var c2 = this.pgnStripped.charAt(k);
						switch (c2) {
							case '_':	//found comment
								break;
							default:	//no comment
								// we might have many comments separated with spaces
								// as we strip all double spaces to single ones we
								// can just check for the next char being '_'
								if (this.pgnStripped.length>k+1
									 	&& this.pgnStripped.charAt(k+1) == '_') {
									continue;
								}
								return [this.pgnRaw.substring(j,k),k];
						}
					}
					break;
				default:	//no comment
					return [null,idx];
			}
		}
		return [null,idx];
	};

Pgn.prototype.isBroken = function (val) {
		var pCount = 0;
		var cCount = 0;
		var lastOne = "";
		var inComment = false;
		for (var i=0;i<val.length;i++) {
			var c = val.charAt(i);
			if(inComment) {
				switch (c) {
					case '}':
						inComment = false;
						// closing a non-existent curly brace
						if (cCount == 0)
							return true;
						// if we're closing a parenthesis instead of a curly
						if (lastOne == "p")
							return true;
						lastOne = "";
						cCount--;
						break;
					case '{':
						// Cannot nest comments
						return true;
				}
			} else {
				switch (c) {
					case '(':
						pCount++;
						lastOne = "p";
						break;
					case ')':
						// closing a non-existent curly brace
						if (pCount == 0)
							return true;
						// closing a curly instead of a parenthesis
						if (lastOne == "c")
							return true;
						lastOne = "";
						pCount--;
						break;
					case '{':
						inComment = true;
						cCount++;
						lastOne = "c";
						break;
				}
			}
		}
		return false;
	};

Pgn.prototype.stripItBroken = function (val, strip) {
	var count = 0;
	var out = new Array();
	/*
		At one point chesspastebin.com started getting cames
		with invalid PGNs, mostly in the form
		{ comment comment ( something between starting brackets}.
		As you can see, the ( is not closed.
		isOpen and isCurlyO are just for that to take normal
		guesses in that kind of situations.
	*/
	var isOpen = false;
	var isCurlyO = false;
	var curlyOpenedFst = false;
	for (var i=0;i<val.length;i++) {
		var c = val.charAt(i);
		switch (c) {
			case '(':
				if (!strip) {
					out[out.length] = '_';
				}
				count++;
				if (isOpen) {
					 	count--;
				}
				isOpen = true;
				break;
			case '{':
				isCurlyO = true;
				if (!strip) {
					out[out.length] = '_';
				}
				count++;
				if (!isOpen)
					 curlyOpenedFst=true;
				break;
			case '}':
				if (isOpen && isCurlyO && curlyOpenedFst) {
					// lets close the open (
					count--;
					isOpen = false;
				}
				isCurlyO = false;
				curlyOpenedFst = false;
				count--;
				if (!strip) {
					out[out.length] = '_';
				}
				break;
			case ')':
				if (isOpen) {
					 count--;
					 if (!strip) {
						 out[out.length] = '_';
					 }
					 isOpen = false;
				}
				break;
			case '\t':
				out[out.length] = ' ';
				break;
			default:
				if (count > 0) {
					if (!strip) {
						out[out.length] = '_';
					}
				}
				else {
					out[out.length] = c;
				}
		}
	}
	return out.join("");
};

/*
	Strip game comments from a PGN string. If second
	parameter set false true then comments will be replaced
	by an underscore.
*/
Pgn.prototype.stripIt = function (val, strip) {
	var count = 0;
	var out = new Array();
	var inComment = false;
	for (var i=0;i<val.length;i++) {
		var c = val.charAt(i);
		switch (c) {
			case '(':
				if (!strip) {
					out[out.length] = '_';
				}
				if(!inComment) {
					count++;
				}
				break;
			case '{':
				inComment = true;
				if (!strip) {
					out[out.length] = '_';
				}
				count++;
				break;
			case '}':
				count--;
				if (!strip) {
					out[out.length] = '_';
				}
				inComment = false;
				break;
			case ')':
				if(!inComment) {
					count--;
				}
				if (!strip) {
					out[out.length] = '_';
				}
				break;
			case '\t':
				out[out.length] = ' ';
				break;
			default:
				if (count > 0) {
					if (!strip) {
						out[out.length] = '_';
					}
				}
				else {
					out[out.length] = c;
				}
		}
	}
	return out.join("");
};
<|endoftext|>