diff --git a/MiniScript-cpp/README.md b/MiniScript-cpp/README.md index 33fe516..53dee52 100644 --- a/MiniScript-cpp/README.md +++ b/MiniScript-cpp/README.md @@ -12,9 +12,9 @@ MiniScript is built with [CMake](https://cmake.org/). You can generate your desi 3. `cmake ../..` (or on Windows, `cmake ..\..`) to generate a makefile and related files in the build directory. -4. `cmake --build .` to actually do the build. +4. `cmake --build . --config Release` to actually do the build. -If successful, you should find an executable called `miniscript` which you can install (see **Installation**, below). You'll also find a shared library (libminiscript-cpp.a) for you to link to in your own projects if you like. You can even `include()` the CMakeLists.txt of this project inside your own for clean dependency management. +If successful, you should find an executable called `miniscript` (or `miniscript.exe` on Windows) which you can install (see **Installation**, below). You'll also find a shared library (libminiscript-cpp.a, miniscript-cpp.lib, or similar) for you to link to in your own projects if you like. You can even `include()` the CMakeLists.txt of this project inside your own for clean dependency management. If you are only interested in the C# edition of MiniScript, there is a project file provided in the respective directory. diff --git a/MiniScript-cpp/lib/listUtil.ms b/MiniScript-cpp/lib/listUtil.ms index 871d4d7..c71aba2 100644 --- a/MiniScript-cpp/lib/listUtil.ms +++ b/MiniScript-cpp/lib/listUtil.ms @@ -9,6 +9,21 @@ list.contains = function(item) return self.indexOf(item) != null end function +// lastIndexOf: returns the *last* index of the given element, optionally +// before a given index. Returns null if not found. +// Examples: +// [7,4,7,9].lastIndexOf(7) // returns 2 +// [7,4,7,9].lastIndexOf(7, 2) // returns 0 +list.lastIndexOf = function(element, beforeIdx=null) + if beforeIdx == null then i = self.len - 1 else i = beforeIdx - 1 + while i >= 0 + if self[i] == element then return i + i -= 1 + end while + return null +end function + + // split: similar to string.split, this splits a list into sub-lists // by where the given delimiter element is found. list.split = function(delimiter, maxCount=null) @@ -161,6 +176,17 @@ list.apply = function(func) end for end function +// applied: like list.apply, but returns the result as a new list, +// without changing the list it is called on. +// Example: +// x = [1.1, 1.9, 3.45] +// y = x.applied(@round) // y is now [1, 2, 3]; x is unchanged +list.applied = function(func) + result = self[:] + result.apply @func + return result +end function + // apply1: same as apply, but takes 1 extra argument. list.apply1 = function(func, arg1) for i in self.indexes @@ -168,6 +194,14 @@ list.apply1 = function(func, arg1) end for end function +// applied1: like list.apply1, but returns the result as a new list, +// without changing the list it is called on. +list.applied1 = function(func, arg1) + result = self[:] + result.apply1 @func, arg1 + return result +end function + // mean: return the average (sum divided by number of values) list.mean = function return self.sum / self.len @@ -273,6 +307,17 @@ list.filter = function(func) end if end function +// filter1: like list.filter, but takes 1 extra argument for the +// filter function. Note that func must be an actual function, +// and not just the name of a map key in this case. +list.filter1 = function(func, arg1) + i = self.len - 1 + while i >= 0 + if not func(self[i], arg1) then self.remove i + i = i - 1 + end while +end function + // filtered: return a new list containing only the elements of self where // the given function is true. As with filter, you may also specify the // name of a map key to look up in each element of the self. @@ -291,6 +336,54 @@ list.filtered = function(func) return result end function +// filtered1: like list.filter, but takes an additional argument +// for the filter function. +list.filtered1 = function(func, arg1) + result = [] + for elem in self + if func(elem, arg1) then result.push elem + end for + return result +end function + +// compress: replace each run of a given value with a single instance +// of that value (in place). +// Example: +// x = [5,7,7,9,7,7,7]; x.compress(7) // x is now [5,7,9,7] +list.compress = function(valueToCompress) + i = self.len - 1 + compressing = false + while i >= 0 + if self[i] == valueToCompress then + if compressing then self.remove i else compressing = true + else + compressing = false + end if + i -= 1 + end while +end function + +// compressed: return a new list that has run of a given value +// replaced with a single instance of that value. +// Example: +// x = [5,7,7,9,7,7,7]; x.compress(7) // x is now [5,7,9,7] +list.compressed = function(valueToCompress) + result = [] + compressing = false + for value in self + if value == valueToCompress then + if not compressing then + result.push value + compressing = true + end if + else + compressing = false + result.push value + end if + end for + return result +end function + // valuesOf: operates on a list of maps (or other indexable type), and pulls // out the value of the given index for every element in the list, returning // theses as a new list in the same order. @@ -374,6 +467,15 @@ runUnitTests = function end if end function + assertEqual [7,4,7,9].lastIndexOf(7), 2, "lastIndexOf" + assertEqual [7,4,7,9].lastIndexOf(7,2), 0, "lastIndexOf" + assertEqual [7,4,7,9].lastIndexOf(7,0), null, "lastIndexOf" + + a = [5,7,7,9,7,7,7] + assertEqual a.compressed(7), [5,7,9,7], "compressed" + assertEqual a, [5,7,7,9,7,7,7], "compressed changed self" + a.compress(7); assertEqual a, [5,7,9,7], "compress" + a = [-1, 42, -1, 53, 87, 345, -1, 100] assertEqual a.split(-1), [ [], [42], [53, 87, 345], [100] ] assertEqual a.split(-1, 3), [ [], [42], [53, 87, 345, -1, 100] ] @@ -410,6 +512,13 @@ runUnitTests = function a.apply1 @round, 2 assertEqual a, [1.10, 1.90, 3.45], "apply1" + a = [1.1, 1.899, 3.452] + b = a.applied(@round) + assertEqual b, [1, 2, 3], "applied" + b = a.applied1(@round, 2) + assertEqual b, [1.10, 1.90, 3.45], "applied1" + assertEqual a, [1.1, 1.899, 3.452], "applied/applied1" + a = range(1, 10) isEven = function(x) return x % 2 == 0 @@ -421,6 +530,15 @@ runUnitTests = function assertEqual a, range(1, 10), "filtered" assertEqual b, [2, 4, 6, 8, 10], "filtered" + moreThan = function(a, b); return a > b; end function + a = range(1,10) + a.filter1 @moreThan, 7 + assertEqual a, [8, 9, 10], "filter1" + a = range(1,10) + b = a.filtered1(@moreThan, 7) + assertEqual a, range(1, 10), "filtered1" + assertEqual b, [8, 9, 10], "filtered1" + a = [{"x":42, "keep":true}, {"x":123, "keep":false}, {"x":0, "keep":true}] a.filter "keep" assertEqual a, [{"x":42, "keep":true}, {"x":0, "keep":true}], "filter by key" diff --git a/MiniScript-cpp/lib/mapUtil.ms b/MiniScript-cpp/lib/mapUtil.ms index 8f99247..01081a5 100644 --- a/MiniScript-cpp/lib/mapUtil.ms +++ b/MiniScript-cpp/lib/mapUtil.ms @@ -7,7 +7,7 @@ map.get = function(key, defaultValue=null) m = self while m - if m.hasIndex(key) then return m[key] + if m.hasIndex(@key) then return m[@key] if not m.hasIndex("__isa") then break m = m.__isa end while @@ -119,12 +119,15 @@ runUnitTests = function end if end function - d = {"one":"ichi", "two":"ni", "three":"san", "four":"shi", "five":"go"} + d = {"one":"ichi", "two":"ni", "three":"san", "four":"shi", "five":"go", @print: "six"} testing = "get" assertEqual d.get("one", 1), "ichi" assertEqual d.get("ten", 10), 10 assertEqual d.get("twenty"), null + assertEqual d.get(@print), "six" + + d.remove @print testing = "hasValue" assertEqual d.hasValue("ni"), true diff --git a/MiniScript-cpp/lib/mathUtil.ms b/MiniScript-cpp/lib/mathUtil.ms index 48bed3f..5437944 100644 --- a/MiniScript-cpp/lib/mathUtil.ms +++ b/MiniScript-cpp/lib/mathUtil.ms @@ -135,21 +135,43 @@ clamp = function(x, minval=0, maxval=1) return x end function +// max: returns the greater of two values. +// (Note: if you have more than two, consider importing listUtil +// and using list.max instead.) +max = function(a, b) + if b > a then return b else return a +end function + +// min: returns the lesser of two values. +// (As above, also consider list.min from listUtil.) +min = function(a, b) + if b < a then return b else return a +end function + // numToStr: converts a number to a string, with a specified precision // (number of digits past the decimal place). Trailing zeros will be // added as needed to get the decimal point at the right place. numToStr = function(n, precision=null) if precision == null then return str(n) - s = str(round(n, precision)) - if precision > 0 then - dotPos = s.indexOf(".") - if dotPos == null then - s = s + "." + "0" * precision - else if dotPos > s.len - precision - 1 then - s = s + "0" * (dotPos - s.len + precision + 1) - end if + if not n isa number or not precision isa number then + print "numToStr error: arguments must be numbers." + return end if - return s + if precision <= 0 or n == 1/0 or n == -1/0 or n != n then return str(round(n, precision)) + negative = n < 0; n = abs(n) + digits = [floor(n)] + for i in range(1, precision) + d = floor(n * 10^i) % 10 + digits.push d + end for + if (n * 10^precision) % 1 >= 0.5 then + for i in range(digits.len - 1) + digits[i] += 1 + if digits[i] < 10 or i == 0 then break + digits[i] = 0 + end for + end if + return "-" * negative + digits.pull + "." + digits.join("") end function runUnitTests = function @@ -175,12 +197,18 @@ runUnitTests = function assertEqual mover.y, 30, "moveTowardsXY" assertEqual moveTowardsXY(mover, target, 10), false - assertEqual numToStr(pi, 2), "3.14" - assertEqual numToStr(1.23, 3), "1.230" - assertEqual numToStr(1.23), "1.23" - assertEqual numToStr(12345.67, -2), "12300" - assertEqual numToStr(2, 3), "2.000" - + assertEqual numToStr(pi, 2), "3.14", "numToStr" + assertEqual numToStr(pi, 4), "3.1416", "numToStr" + assertEqual numToStr(pi, 12), "3.141592653590", "numToStr" + assertEqual numToStr(1.23, 3), "1.230", "numToStr" + assertEqual numToStr(1.23), "1.23", "numToStr" + assertEqual numToStr(12345.67, -2), "12300", "numToStr" + assertEqual numToStr(2, 3), "2.000", "numToStr" + assertEqual numToStr(2/3, 12), "0.666666666667", "numToStr" + assertEqual numToStr(41.9999, 2), "42.00", "numToStr" + assertEqual numToStr(42 - 1E-12, 5), "42.00000", "numToStr" + assertEqual numToStr(-pi, 4), "-3.1416", "numToStr" + if errorCount == 0 then print "All tests passed. Woot!" else diff --git a/MiniScript-cpp/lib/matrixUtil.ms b/MiniScript-cpp/lib/matrixUtil.ms index 81c2d83..da55223 100644 --- a/MiniScript-cpp/lib/matrixUtil.ms +++ b/MiniScript-cpp/lib/matrixUtil.ms @@ -82,6 +82,11 @@ end function // Instance methods: call these on a Matrix instance. //---------------------------------------------------------------------- +// m.size: return [rows, columns] size of the matrix +Matrix.size = function + return [self.rows, self.columns] +end function + // m.row: return the given row of matrix m as a single-row matrix. Matrix.row = function(zeroBasedRowNum) return Matrix.fromList(self.elem[zeroBasedRowNum]) @@ -197,6 +202,11 @@ Matrix.times = function(m2) if m2 isa Matrix then m2 = m2.elem if m2 isa list then // matrix multiplication + if m2.len != self.columns then + print "Matrix.times error: incompatible sizes " + + self.size + " and " + [m2.len, len(m2[0])] + exit + end if result = Matrix.ofSize(self.rows, m2[0].len) for r in result.rowRange resultRow = result.elem[r] @@ -246,26 +256,32 @@ Matrix.equals = function(m2) return self.elem == m2elems end function +// Matrix.round: rounds all numbers in the matrix. +Matrix.round = function(decimalPlaces=0) + for row in self.elem + for i in row.indexes + row[i] = round(row[i], decimalPlaces) + end for + end for +end function + // Matrix.print: prints matrix data, with some formatting options. // fieldWidth: minimum number of characters for each element // precision: if non-null, round each element to this many digits // columnSep: extra string printed between elements within a row // rowSep: extra string printed between rows; defaults to text.delimiter Matrix.print = function(fieldWidth=10, precision=null, columnSep="", rowSep=null) - oldDelim = text.delimiter; text.delimiter = "" - if rowSep == null then rowSep = oldDelim + if rowSep == null then rowSep = text.delimiter for row in self.elem - firstInRow = true + line = [] for elem in row s = mathUtil.numToStr(elem, precision) + if s.len >= fieldWidth and s.indexOf(".") != null then s = s[:fieldWidth-1] if s.len < fieldWidth then s = " "*(fieldWidth-s.len) + s - if not firstInRow then print columnSep - print s - firstInRow = false + line.push s end for - print rowSep + print line.join(columnSep), rowSep end for - text.delimiter = oldDelim end function diff --git a/MiniScript-cpp/lib/qa.ms b/MiniScript-cpp/lib/qa.ms index 0e46f9b..accb242 100644 --- a/MiniScript-cpp/lib/qa.ms +++ b/MiniScript-cpp/lib/qa.ms @@ -32,10 +32,10 @@ end function // assertEqual: abort if the first two parameters are not equal. // Additional descriptive note is optional. assertEqual = function(actual, expected, note) - if actual == expected then return + if @actual == @expected then return msg = "Assert failed" if note != null then msg = msg + " (" + note + ")" - msg = msg + ": expected `" + expected + "`, but got `" + actual + "`" + msg = msg + ": expected `" + @expected + "`, but got `" + @actual + "`" abort msg end function @@ -50,7 +50,9 @@ typeOf = function(value) if value isa map then mapType = value if value.hasIndex("__isa") then mapType = value.__isa - if namedMaps.hasIndex(mapType) then return namedMaps[mapType] + for kv in namedMaps + if refEquals(kv.key, mapType) then return kv.value + end for return "map" end if return "unknown" @@ -59,11 +61,11 @@ end function // assertType: abort if the first parameter is not of the specified type. // Additional descriptive note is optional. assertType = function(value, type, note) - if value isa type then return + if @value isa type then return msg = "Assert failed" if note != null then msg = msg + " (" + note + ")" msg = msg + ": expected type " + namedMaps[type] + - ", but got a " + typeOf(value) + " (" + value + ")" + ", but got a " + typeOf(@value) + " (" + @value + ")" abort msg end function diff --git a/MiniScript-cpp/lib/stringUtil.ms b/MiniScript-cpp/lib/stringUtil.ms index 221427c..4802f93 100644 --- a/MiniScript-cpp/lib/stringUtil.ms +++ b/MiniScript-cpp/lib/stringUtil.ms @@ -56,6 +56,21 @@ string.contains = function(s) return self.indexOf(s) != null end function +// lastIndexOf: returns the *last* index of the given substring, optionally +// before a given index. Returns null if not found. +// Examples: +// "Hello".lastIndexOf("l") // returns 3 +// "Hello".lastIndexOf("l", 3) // returns 2 +string.lastIndexOf = function(substr, beforeIdx=null) + sslen = substr.len + if beforeIdx == null then i = self.len - sslen else i = beforeIdx - 1 + while i >= 0 + if self[i:i+sslen] == substr then return i + i -= 1 + end while + return null +end function + // pad: return this string, padded (and optionally cut) to a desired length. string.pad = function(length, padChar=" ", cutIfTooLong=true) if self.len > length then @@ -111,6 +126,22 @@ string.trimLeft = function(charsToRemove=null) return self[p0:] end function +// string.compress: replaces any runs of a given character with a +// single instance of that character. +// Example: +// "Hi!!! Bob!!".compress("!") // returns "Hi! Bob!" +string.compress = function(charToCompress=" ") + s = self + prevLen = -1 + charTimes2 = charToCompress + charToCompress + while true + s = s.replace(charTimes2, charToCompress) + if s.len == prevLen then break + prevLen = s.len + end while + return s +end function + // ellideEnd: shortens a string (if needed) by cutting and adding // an ellipsis to the end. // Example: @@ -382,6 +413,14 @@ runUnitTests = function assertEqual "hello world".contains("lo"), true assertEqual "hi".contains("hello"), false + testing = "lastIndexOf" + assertEqual "Hello world".lastIndexOf("l"), 9 + assertEqual "Hello world".lastIndexOf("l", 9), 3 + assertEqual "Hello world".lastIndexOf("l", 3), 2 + assertEqual "Hello world".lastIndexOf("l", 2), null + assertEqual "big billy".lastIndexOf("bi"), 4 + assertEqual "big billy".lastIndexOf("bi", 4), 0 + testing = "pad" assertEqual "foo".pad(5), "foo " assertEqual "foo".pad(2), "fo" @@ -401,6 +440,14 @@ runUnitTests = function testing = "trimLeft"; assertEqual s.trimLeft, s testing = "trimRight"; assertEqual s.trimRight, s + testing = "compress" + s = "Hi.... Bob. SHHH" + assertEqual s.compress, "Hi.... Bob. SHHH" + assertEqual s.compress("."), "Hi. Bob. SHHH" + assertEqual s.compress("H"), "Hi.... Bob. SH" + assertEqual "ooOOoo".compress("o"), "oOOo" + assertEqual "ooOoo".compress("O"), "ooOoo" + testing = "reverse" assertEqual "Hello world!".reverse, "!dlrow olleH"