Merge branch 'develop' into declare-test

* develop:
  Added type to appease bisect
  Minor additions to datetime units
  Trying to appease the CI tests, which are failing here for some reason.
  Delete Reducer_Peggy_GeneratedParser.js
  Formatted Code
  Moved DateTime functionality into separate file
  remove unit evaluation test
  generic unit support
  numbers with units (tested)
  Cleaned up arithmetic operations of time durations
  Simple better format for Duration toString
  More sophisticated makeFromYear function, that accepts floats
  Added better Duration toString and Date makeWithYear functionality
  Simple dateTime integration
This commit is contained in:
Ozzie Gooen 2022-05-24 16:26:32 -04:00
commit 9185719641
10 changed files with 215 additions and 7 deletions

View File

@ -189,6 +189,19 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
{expression.value.map((r) => `"${r}"`).join(", ")} {expression.value.map((r) => `"${r}"`).join(", ")}
</VariableBox> </VariableBox>
); );
case "date":
return (
<VariableBox heading="Date" showTypes={showTypes}>
{expression.value.toDateString()}
</VariableBox>
);
case "timeDuration": {
return (
<VariableBox heading="Time Duration" showTypes={showTypes}>
<NumberShower precision={3} number={expression.value} />
</VariableBox>
);
}
case "lambda": case "lambda":
return ( return (
<FunctionChart <FunctionChart

View File

@ -251,6 +251,10 @@ describe("Peggy parse", () => {
"{(::map (::$_constructArray_$ (1 2 3)) {|:x| {(::add :x 1)}})}", "{(::map (::$_constructArray_$ (1 2 3)) {|:x| {(::add :x 1)}})}",
) )
}) })
describe("unit", () => {
testParse("1m", "{(::fromUnit_m 1)}")
testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}")
})
}) })
describe("parsing new line", () => { describe("parsing new line", () => {

View File

@ -181,5 +181,9 @@ function createTsExport(
return tag("string", x.value); return tag("string", x.value);
case "EvSymbol": case "EvSymbol":
return tag("symbol", x.value); return tag("symbol", x.value);
case "EvDate":
return tag("date", x.value);
case "EvTimeDuration":
return tag("timeDuration", x.value);
} }
} }

View File

@ -55,6 +55,14 @@ export type rescriptExport =
| { | {
TAG: 9; // EvSymbol TAG: 9; // EvSymbol
_0: string; _0: string;
}
| {
TAG: 10; // EvDate
_0: Date;
}
| {
TAG: 11; // EvTimeDuration
_0: number;
}; };
type rescriptDist = type rescriptDist =
@ -86,6 +94,8 @@ export type squiggleExpression =
| tagged<"boolean", boolean> | tagged<"boolean", boolean>
| tagged<"distribution", Distribution> | tagged<"distribution", Distribution>
| tagged<"number", number> | tagged<"number", number>
| tagged<"date", Date>
| tagged<"timeDuration", number>
| tagged<"record", { [key: string]: squiggleExpression }>; | tagged<"record", { [key: string]: squiggleExpression }>;
export { lambdaValue }; export { lambdaValue };
@ -127,6 +137,10 @@ export function convertRawToTypescript(
return tag("string", result._0); return tag("string", result._0);
case 9: // EvSymbol case 9: // EvSymbol
return tag("symbol", result._0); return tag("symbol", result._0);
case 10: // EvDate
return tag("date", result._0);
case 11: // EvTimeDuration
return tag("number", result._0);
} }
} }

View File

@ -4,18 +4,19 @@ type errorValue =
| REArrayIndexNotFound(string, int) | REArrayIndexNotFound(string, int)
| REAssignmentExpected | REAssignmentExpected
| REDistributionError(DistributionTypes.error) | REDistributionError(DistributionTypes.error)
| REOperationError(Operation.operationError) | REExpectedType(string)
| REExpressionExpected | REExpressionExpected
| REFunctionExpected(string) | REFunctionExpected(string)
| REFunctionNotFound(string) | REFunctionNotFound(string)
| REJavaScriptExn(option<string>, option<string>) // Javascript Exception | REJavaScriptExn(option<string>, option<string>) // Javascript Exception
| REMacroNotFound(string) | REMacroNotFound(string)
| RENotAFunction(string) | RENotAFunction(string)
| REOperationError(Operation.operationError)
| RERecordPropertyNotFound(string, string) | RERecordPropertyNotFound(string, string)
| RESymbolNotFound(string) | RESymbolNotFound(string)
| RESyntaxError(string) | RESyntaxError(string)
| RETodo(string) // To do | RETodo(string) // To do
| REExpectedType(string) | REUnitNotFound(string)
type t = errorValue type t = errorValue
@ -52,4 +53,5 @@ let errorToString = err =>
| RESyntaxError(desc) => `Syntax Error: ${desc}` | RESyntaxError(desc) => `Syntax Error: ${desc}`
| RETodo(msg) => `TODO: ${msg}` | RETodo(msg) => `TODO: ${msg}`
| REExpectedType(typeName) => `Expected type: ${typeName}` | REExpectedType(typeName) => `Expected type: ${typeName}`
| REUnitNotFound(unitName) => `Unit not found: ${unitName}`
} }

View File

@ -248,8 +248,7 @@ basicValue = valueConstructor / basicLiteral
basicLiteral basicLiteral
= string = string
/ float / number
/ integer
/ boolean / boolean
/ dollarIdentifier / dollarIdentifier
@ -263,6 +262,15 @@ string 'string'
= characters:("'" @([^'])* "'") {return nodeString(characters.join(''))} = characters:("'" @([^'])* "'") {return nodeString(characters.join(''))}
/ characters:('"' @([^"])* '"') {return nodeString(characters.join(''))} / characters:('"' @([^"])* '"') {return nodeString(characters.join(''))}
number = number:(float / integer) unit:identifier?
{
if (unit === null)
{ return number }
else
{ return makeFunctionCall('fromUnit_'+unit.value, [number])
}
}
integer 'integer' integer 'integer'
= d+ !"\." ![e]i = d+ !"\." ![e]i
{ return nodeInteger(parseInt(text()))} { return nodeInteger(parseInt(text()))}

View File

@ -0,0 +1,70 @@
module EV = ReducerInterface_ExpressionValue
type expressionValue = EV.expressionValue
let dateDispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch call {
| ("toString", [EvDate(t)]) => EV.EvString(DateTime.Date.toString(t))->Ok->Some
| ("makeDateFromYear", [EvNumber(year)]) =>
switch DateTime.Date.makeFromYear(year) {
| Ok(t) => EV.EvDate(t)->Ok->Some
| Error(e) => Reducer_ErrorValue.RETodo(e)->Error->Some
}
| ("dateFromNumber", [EvNumber(f)]) => EV.EvDate(DateTime.Date.fromFloat(f))->Ok->Some
| ("toNumber", [EvDate(f)]) => EV.EvNumber(DateTime.Date.toFloat(f))->Ok->Some
| ("subtract", [EvDate(d1), EvDate(d2)]) =>
switch DateTime.Date.subtract(d1, d2) {
| Ok(d) => EV.EvTimeDuration(d)->Ok
| Error(e) => Error(RETodo(e))
}->Some
| ("subtract", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.subtractDuration(d1, d2))->Ok->Some
| ("add", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.addDuration(d1, d2))->Ok->Some
| _ => None
}
}
let durationDispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch call {
| ("toString", [EvTimeDuration(t)]) => EV.EvString(DateTime.Duration.toString(t))->Ok->Some
| ("minutes", [EvNumber(f)]) => EV.EvTimeDuration(DateTime.Duration.fromMinutes(f))->Ok->Some
| ("fromUnit_minutes", [EvNumber(f)]) =>
EV.EvTimeDuration(DateTime.Duration.fromMinutes(f))->Ok->Some
| ("hours", [EvNumber(f)]) => EV.EvTimeDuration(DateTime.Duration.fromHours(f))->Ok->Some
| ("fromUnit_hours", [EvNumber(f)]) => EV.EvTimeDuration(DateTime.Duration.fromHours(f))->Ok->Some
| ("days", [EvNumber(f)]) => EV.EvTimeDuration(DateTime.Duration.fromDays(f))->Ok->Some
| ("fromUnit_days", [EvNumber(f)]) => EV.EvTimeDuration(DateTime.Duration.fromDays(f))->Ok->Some
| ("years", [EvNumber(f)]) => EV.EvTimeDuration(DateTime.Duration.fromYears(f))->Ok->Some
| ("fromUnit_years", [EvNumber(f)]) => EV.EvTimeDuration(DateTime.Duration.fromYears(f))->Ok->Some
| ("toHours", [EvTimeDuration(f)]) => EV.EvNumber(DateTime.Duration.toHours(f))->Ok->Some
| ("toMinutes", [EvTimeDuration(f)]) => EV.EvNumber(DateTime.Duration.toMinutes(f))->Ok->Some
| ("toDays", [EvTimeDuration(f)]) => EV.EvNumber(DateTime.Duration.toDays(f))->Ok->Some
| ("toYears", [EvTimeDuration(f)]) => EV.EvNumber(DateTime.Duration.toYears(f))->Ok->Some
| ("add", [EvTimeDuration(d1), EvTimeDuration(d2)]) =>
EV.EvTimeDuration(DateTime.Duration.add(d1, d2))->Ok->Some
| ("subtract", [EvTimeDuration(d1), EvTimeDuration(d2)]) =>
EV.EvTimeDuration(DateTime.Duration.subtract(d1, d2))->Ok->Some
| ("multiply", [EvTimeDuration(d1), EvNumber(d2)]) =>
EV.EvTimeDuration(DateTime.Duration.multiply(d1, d2))->Ok->Some
| ("divide", [EvTimeDuration(d1), EvNumber(d2)]) =>
EV.EvTimeDuration(DateTime.Duration.divide(d1, d2))->Ok->Some
| _ => None
}
}
let dispatch = (call: EV.functionCall, env: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch dateDispatch(call, env) {
| Some(r) => Some(r)
| None =>
switch durationDispatch(call, env) {
| Some(r) => Some(r)
| None => None
}
}
}

View File

@ -20,6 +20,8 @@ type rec expressionValue =
| EvRecord(record) | EvRecord(record)
| EvString(string) | EvString(string)
| EvSymbol(string) | EvSymbol(string)
| EvDate(Js.Date.t)
| EvTimeDuration(float)
and record = Js.Dict.t<expressionValue> and record = Js.Dict.t<expressionValue>
and externalBindings = record and externalBindings = record
and lambdaValue = { and lambdaValue = {
@ -51,6 +53,8 @@ let rec toString = aValue =>
| EvSymbol(aString) => `:${aString}` | EvSymbol(aString) => `:${aString}`
| EvRecord(aRecord) => aRecord->toStringRecord | EvRecord(aRecord) => aRecord->toStringRecord
| EvDistribution(dist) => GenericDist.toString(dist) | EvDistribution(dist) => GenericDist.toString(dist)
| EvDate(date) => DateTime.Date.toString(date)
| EvTimeDuration(t) => DateTime.Duration.toString(t)
} }
and toStringRecord = aRecord => { and toStringRecord = aRecord => {
let pairs = let pairs =
@ -73,6 +77,8 @@ let toStringWithType = aValue =>
| EvRecord(_) => `Record::${toString(aValue)}` | EvRecord(_) => `Record::${toString(aValue)}`
| EvString(_) => `String::${toString(aValue)}` | EvString(_) => `String::${toString(aValue)}`
| EvSymbol(_) => `Symbol::${toString(aValue)}` | EvSymbol(_) => `Symbol::${toString(aValue)}`
| EvDate(_) => `Date::${toString(aValue)}`
| EvTimeDuration(_) => `Date::${toString(aValue)}`
} }
let argsToString = (args: array<expressionValue>): string => { let argsToString = (args: array<expressionValue>): string => {
@ -116,6 +122,8 @@ type expressionValueType =
| EvtRecord | EvtRecord
| EvtString | EvtString
| EvtSymbol | EvtSymbol
| EvtDate
| EvtTimeDuration
type functionCallSignature = CallSignature(string, array<expressionValueType>) type functionCallSignature = CallSignature(string, array<expressionValueType>)
type functionDefinitionSignature = type functionDefinitionSignature =
@ -133,6 +141,8 @@ let valueToValueType = value =>
| EvRecord(_) => EvtRecord | EvRecord(_) => EvtRecord
| EvString(_) => EvtArray | EvString(_) => EvtArray
| EvSymbol(_) => EvtSymbol | EvSymbol(_) => EvtSymbol
| EvDate(_) => EvtDate
| EvTimeDuration(_) => EvtTimeDuration
} }
let functionCallToCallSignature = (functionCall: functionCall): functionCallSignature => { let functionCallToCallSignature = (functionCall: functionCall): functionCallSignature => {
@ -152,6 +162,8 @@ let valueTypeToString = (valueType: expressionValueType): string =>
| EvtRecord => `Record` | EvtRecord => `Record`
| EvtString => `String` | EvtString => `String`
| EvtSymbol => `Symbol` | EvtSymbol => `Symbol`
| EvtDate => `Date`
| EvtTimeDuration => `Duration`
} }
let functionCallSignatureToString = (functionCallSignature: functionCallSignature): string => { let functionCallSignatureToString = (functionCallSignature: functionCallSignature): string => {

View File

@ -18,9 +18,11 @@ let dispatch = (call: ExpressionValue.functionCall, environment, chain): result<
expressionValue, expressionValue,
'e, 'e,
> => > =>
ReducerInterface_GenericDistribution.dispatch(call, environment) |> E.O.default( switch ReducerInterface_GenericDistribution.dispatch(call, environment) {
chain(call, environment), | Some(r) => r
) | None =>
ReducerInterface_DateTime.dispatch(call, environment) |> E.O.default(chain(call, environment))
}
/* /*
If your dispatch is too big you can divide it into smaller dispatches and pass the call so that it gets called finally. If your dispatch is too big you can divide it into smaller dispatches and pass the call so that it gets called finally.

View File

@ -0,0 +1,79 @@
module Duration = {
//Stores in Unix milliseconds
type t = float
let minute = Belt.Float.fromInt(60 * 1000)
let hour = Belt.Float.fromInt(60 * 60 * 1000)
let day = Belt.Float.fromInt(24 * 60 * 60 * 1000)
let year = Belt.Float.fromInt(24 * 60 * 60 * 1000) *. 365.25
let fromFloat = (f: float): t => f
let toFloat = (d: t): float => d
let fromMinutes = (h: float): t => h *. minute
let fromHours = (h: float): t => h *. hour
let fromDays = (d: float): t => d *. day
let fromYears = (y: float): t => y *. year
let toMinutes = (t: t): float => t /. minute
let toHours = (t: t): float => t /. hour
let toDays = (t: t): float => t /. day
let toYears = (t: t): float => t /. year
let toString = (t: t): string => {
let shouldPluralize = f => f != 1.0
let display = (f: float, s: string) =>
`${E.Float.with3DigitsPrecision(f)} ${s}${shouldPluralize(f) ? "s" : ""}`
let abs = Js.Math.abs_float(t)
if abs >= year {
display(t /. year, "year")
} else if abs >= day {
display(t /. day, "day")
} else if abs >= hour {
display(t /. hour, "hour")
} else if abs >= minute {
display(t /. minute, "minute")
} else {
E.Float.toFixed(t) ++ "ms"
}
}
let add = (t1: t, t2: t): t => t1 +. t2
let subtract = (t1: t, t2: t): t => t1 -. t2
let multiply = (t1: t, t2: float): t => t1 *. t2
let divide = (t1: t, t2: float): t => t1 /. t2
}
module Date = {
//The Rescript/JS implementation of Date is pretty mediocre. It would be good to improve upon later.
type t = Js.Date.t
let toFloat = Js.Date.getTime
let getFullYear = Js.Date.getFullYear
let toString = Js.Date.toDateString
let fromFloat = Js.Date.fromFloat
let fmap = (t: t, fn: float => float) => t->toFloat->fn->fromFloat
let subtract = (t1: t, t2: t) => {
let (f1, f2) = (toFloat(t1), toFloat(t2))
let diff = f1 -. f2
if diff < 0.0 {
Error("Cannot subtract a date by one that is in its future")
} else {
Ok(Duration.fromFloat(diff))
}
}
let addDuration = (t: t, duration: Duration.t) => fmap(t, t => t +. duration)
let subtractDuration = (t: t, duration: Duration.t) => fmap(t, t => t -. duration)
//The Js.Date.makeWithYM function accepts a float, but only treats it as a whole number.
//Our version takes an integer to make this distinction clearer.
let makeWithYearInt = (y: int): result<t, string> => {
if y < 100 {
Error("Year must be over 100")
} else if y > 200000 {
Error("Year must be less than 200000")
} else {
Ok(Js.Date.makeWithYM(~year=Belt.Float.fromInt(y), ~month=0.0, ()))
}
}
let makeFromYear = (year: float): result<t, string> => {
let floor = year->Js.Math.floor_float
makeWithYearInt(Belt.Float.toInt(floor))->E.R2.fmap(earlyDate => {
let diff = year -. floor
earlyDate->addDuration(diff *. Duration.year)
})
}
}