diff --git a/doc/function/sort_list.txt b/doc/function/sort_list.txt index c202ef12..426489c0 100644 --- a/doc/function/sort_list.txt +++ b/doc/function/sort_list.txt @@ -10,9 +10,12 @@ Optionally order by some other property. ! Parameter Type Description | @input@ [[type:list]] or [[type:set]] List to sort. | @order_by@ [[type:function]] (optional) Function to order by, for example when @order_by: {input.name}@ orders items by their name property. +| @remove_duplicates@ [[type:boolean]] DOC_MSE_VERSION: since 0.3.7 + If @true@, keep only one copy of duplicate elements. --Examples-- > sort_list([5,2,3,1,4]) == [1,2,3,4,5] > sort_list(["aaa","cccc","bb"]) == ["aaa","bb","cccc"] -> sort_list(["aaa","cccc","bb"], order_by: {number_of_items(in:input)}) +> sort_list(["aaa","cccc","bb"], order_by: length) > == ["bb","aaa","cccc"] +> sort_list([1,2,1,2,2,3], remove_duplicates:true) == [1,2,3] diff --git a/doc/type/tagged_string.txt b/doc/type/tagged_string.txt index 6111068d..c4c8008a 100644 --- a/doc/type/tagged_string.txt +++ b/doc/type/tagged_string.txt @@ -16,7 +16,7 @@ This is written as the character with code 1 in files. | @""@ The text inside the tag is italic. | @""@ The text inside the tag is rendered as symbols, if a [[prop:style:symbol font]] is set for the text box. | @""@ The text inside the tag is rendered with the given [[type:color]]. -| @""@ The text inside the tag is scaled by the a factor, for example @"text"@ makes the text twice as large. +| @""@ The text inside the tag is rendered with the given font size in points, for example @"text"@ makes the text 12 points. The text is scaled down proportianally when it does not fit in a text box and the @scale down to@ attribute allows it. | @""@ Line breaks inside this tag use the [[prop:style:line height line]], and they show a horizontal line. | @""@ Line breaks inside this tag use the [[prop:style:soft line height]]. | @""@ An atomic piece of text. The cursor can never be inside it; it is selected as a whole. diff --git a/src/script/functions/basic.cpp b/src/script/functions/basic.cpp index eeb60608..ae86dac4 100644 --- a/src/script/functions/basic.cpp +++ b/src/script/functions/basic.cpp @@ -33,7 +33,26 @@ SCRIPT_FUNCTION(trace) { SCRIPT_FUNCTION(warning) { SCRIPT_PARAM_C(String, input); - handle_warning(input, true); + SCRIPT_PARAM_DEFAULT_C(bool, condition, true); + if (condition) { + handle_warning(input, true); + } + return script_nil; +} +SCRIPT_FUNCTION(warning_if_neq) { + SCRIPT_PARAM_C(String, input); + SCRIPT_PARAM_N(ScriptValueP, SCRIPT_VAR__1, v1); + SCRIPT_PARAM_N(ScriptValueP, SCRIPT_VAR__2, v2); + if (!equal(v1,v2)) { + String s1 = _("?"), s2 = _("?"); + try { + s1 = v1->toCode(); + } catch (...) {} + try { + s2 = v2->toCode(); + } catch (...) {} + handle_warning(input + s1 + _(" != ") + s2, true); + } return script_nil; } @@ -59,13 +78,17 @@ String format_input(const String& format, Context& ctx) { SCRIPT_FUNCTION(to_string) { ScriptValueP format = ctx.getVariable(SCRIPT_VAR_format); - if (format && format->type() == SCRIPT_STRING) { - // format specifier. Be careful, the built in function 'format' has the same name - SCRIPT_RETURN(format_input(*format, ctx)); - } else { - // simple conversion - SCRIPT_PARAM_C(String, input); - SCRIPT_RETURN(input); + try { + if (format && format->type() == SCRIPT_STRING) { + // format specifier. Be careful, the built in function 'format' has the same name + SCRIPT_RETURN(format_input(*format, ctx)); + } else { + // simple conversion + SCRIPT_PARAM_C(String, input); + SCRIPT_RETURN(input); + } + } catch (const ScriptError& e) { + return new_intrusive1(e); } } @@ -145,6 +168,11 @@ SCRIPT_FUNCTION(to_color) { } } +SCRIPT_FUNCTION(to_code) { + SCRIPT_PARAM_C(ScriptValueP, input); + SCRIPT_RETURN(input->toCode()); +} + // ----------------------------------------------------------------------------- : Math SCRIPT_FUNCTION(abs) { @@ -344,14 +372,20 @@ int position_in_vector(const ScriptValueP& of, const ScriptValueP& in, const Scr inline bool smart_less_first(const pair& a, const pair& b) { return smart_less(a.first, b.first); } +inline bool smart_equal_first(const pair& a, const pair& b) { + return smart_equal(a.first, b.first); +} // sort a script list -ScriptValueP sort_script(Context& ctx, const ScriptValueP& list, ScriptValue& order_by) { +ScriptValueP sort_script(Context& ctx, const ScriptValueP& list, ScriptValue& order_by, bool remove_duplicates) { ScriptType list_t = list->type(); if (list_t == SCRIPT_STRING) { // sort a string String s = list->toString(); sort(s.begin(), s.end()); + if (remove_duplicates) { + s.erase( unique(s.begin(), s.end()), s.end() ); + } SCRIPT_RETURN(s); } else { // are we sorting a set? @@ -364,6 +398,10 @@ ScriptValueP sort_script(Context& ctx, const ScriptValueP& list, ScriptValue& or values.push_back(make_pair(order_by.eval(ctx)->toString(), v)); } sort(values.begin(), values.end(), smart_less_first); + // unique + if (remove_duplicates) { + values.erase( unique(values.begin(), values.end(), smart_equal_first), values.end() ); + } // return collection ScriptCustomCollectionP ret(new ScriptCustomCollection()); FOR_EACH(v, values) { @@ -442,10 +480,12 @@ SCRIPT_FUNCTION(filter_list) { SCRIPT_FUNCTION(sort_list) { SCRIPT_PARAM_C(ScriptValueP, input); - SCRIPT_PARAM_N(ScriptValueP, _("order by"), order_by); - return sort_script(ctx, input, *order_by); + SCRIPT_PARAM_DEFAULT_N(ScriptValueP, _("order by"), order_by, script_nil); + SCRIPT_PARAM_DEFAULT_N(bool, _("remove duplicates"), remove_duplicates, false); + return sort_script(ctx, input, *order_by, remove_duplicates); } + SCRIPT_FUNCTION(random_shuffle) { SCRIPT_PARAM_C(ScriptValueP, input); // convert to CustomCollection @@ -571,6 +611,7 @@ void init_script_basic_functions(Context& ctx) { ctx.setVariable(_("to number"), script_to_number); ctx.setVariable(_("to boolean"), script_to_boolean); ctx.setVariable(_("to color"), script_to_color); + ctx.setVariable(_("to code"), script_to_code); // math ctx.setVariable(_("abs"), script_abs); ctx.setVariable(_("random_real"), script_random_real); diff --git a/src/script/parser.cpp b/src/script/parser.cpp index af19c339..d8b6a0cf 100644 --- a/src/script/parser.cpp +++ b/src/script/parser.cpp @@ -23,6 +23,7 @@ DECLARE_TYPEOF_COLLECTION(Variable); String read_utf8_line(wxInputStream& input, bool eat_bom = true, bool until_eof = false); extern ScriptValueP script_warning; +extern ScriptValueP script_warning_if_neq; // ----------------------------------------------------------------------------- : Tokenizing : class @@ -557,45 +558,30 @@ void parseExpr(TokenIterator& input, Script& script, Precedence minPrec) { expectToken(input, _("(")); size_t start = input.peek().pos; int line = input.getLineNumber(); + size_t function_pos = script.getConstants().size(); + script.addInstruction(I_PUSH_CONST, script_warning); parseOper(input, script, PREC_ALL); // condition size_t end = input.peek().pos; String message = String::Format(_("Assertion failure on line %d:\n expected: "), line) + input.getSourceCode(start,end); expectToken(input, _(")"), &token); if (script.getInstructions().back().instr == I_BINARY && script.getInstructions().back().instr2 == I_EQ) { - // compile "assert(x == y)" into " + // compile "assert(x == y)" into + // warning_if_neq("condition", _1: x, _2: y) message += _("\n found: "); - script.getInstructions().pop_back(); // remove == - script.addInstruction(I_DUP, 1); // duplicate X - script.addInstruction(I_DUP, 1); // duplicate Y - script.addInstruction(I_BINARY, I_EQ); // - unsigned jmpElse = script.addInstruction(I_JUMP_IF_NOT); // jnz lbl_else - script.addInstruction(I_PUSH_CONST, script_nil); // push nil - unsigned jmpEnd = script.addInstruction(I_JUMP); // jump lbl_end - script.comeFrom(jmpElse); // lbl_else: - script.addInstruction(I_PUSH_CONST, script_warning); // push warning - script.addInstruction(I_PUSH_CONST, message); // push "condition" - script.addInstruction(I_DUP, 3); // duplicate X - script.addInstruction(I_BINARY, I_ADD); // add - script.addInstruction(I_PUSH_CONST, String(_(" != "))); // push " != " - script.addInstruction(I_BINARY, I_ADD); // add - script.addInstruction(I_DUP, 2); // duplicate Y - script.addInstruction(I_BINARY, I_ADD); // add - script.addInstruction(I_CALL, 1); // call - script.addInstruction(I_NOP, SCRIPT_VAR_input); // (input:) - script.comeFrom(jmpEnd); // lbl_end: - script.addInstruction(I_BINARY, I_POP); // pop Y_copy - script.addInstruction(I_BINARY, I_POP); // pop X_copy - } else { - // compile into: if condition then nil else warning("condition") - unsigned jmpElse = script.addInstruction(I_JUMP_IF_NOT); // jnz lbl_else - script.addInstruction(I_PUSH_CONST, script_nil); // push nil - unsigned jmpEnd = script.addInstruction(I_JUMP); // jump lbl_end - script.comeFrom(jmpElse); // lbl_else: - script.addInstruction(I_PUSH_CONST, script_warning); // push warning + script.getConstants()[function_pos] = script_warning_if_neq; + script.getInstructions().pop_back(); // POP == instruction script.addInstruction(I_PUSH_CONST, message); // push "condition" - script.addInstruction(I_CALL, 1); // call + script.addInstruction(I_CALL, 3); // call + script.addInstruction(I_NOP, SCRIPT_VAR__1); // (_1:) + script.addInstruction(I_NOP, SCRIPT_VAR__2); // (_2:) + script.addInstruction(I_NOP, SCRIPT_VAR_input); // (input:) + } else { + // compile into: warning("condition", condition: not condition) + script.addInstruction(I_UNARY, I_NOT); // not + script.addInstruction(I_PUSH_CONST, message); // push "condition" + script.addInstruction(I_CALL, 2); // call + script.addInstruction(I_NOP, SCRIPT_VAR_condition); // (condition:) script.addInstruction(I_NOP, SCRIPT_VAR_input); // (input:) - script.comeFrom(jmpEnd); // lbl_end: } } else { // variable diff --git a/src/script/script.cpp b/src/script/script.cpp index c22c49d1..4b17af3c 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -52,6 +52,8 @@ void init_script_variables() { #define VarN(X,name) if (SCRIPT_VAR_##X != string_to_variable(name)) assert(false); #define Var(X) VarN(X,_(#X)) Var(input); + Var(_1); + Var(_2); Var(in); Var(match); Var(replace); @@ -73,6 +75,7 @@ void init_script_variables() { Var(card); Var(styling); Var(value); + Var(condition); assert(variables.size() == SCRIPT_VAR_CUSTOM_FIRST); } diff --git a/src/script/script.hpp b/src/script/script.hpp index 77ef22cf..1e6b092b 100644 --- a/src/script/script.hpp +++ b/src/script/script.hpp @@ -111,6 +111,8 @@ struct Instruction { // for faster lookup from code enum Variable { SCRIPT_VAR_input +, SCRIPT_VAR__1 +, SCRIPT_VAR__2 , SCRIPT_VAR_in , SCRIPT_VAR_match , SCRIPT_VAR_replace @@ -132,6 +134,7 @@ enum Variable , SCRIPT_VAR_card , SCRIPT_VAR_styling , SCRIPT_VAR_value +, SCRIPT_VAR_condition , SCRIPT_VAR_CUSTOM_FIRST // other variables start from here , SCRIPT_VAR_CUSTOM_LOTS = 0xFFFFFF // ensure that sizeof(Variable) is large enough }; diff --git a/src/script/to_value.hpp b/src/script/to_value.hpp index f9275705..f7814eb6 100644 --- a/src/script/to_value.hpp +++ b/src/script/to_value.hpp @@ -95,6 +95,12 @@ ScriptValueP rangeIterator(int start, int end); // ----------------------------------------------------------------------------- : Collections +class ScriptCollectionBase : public ScriptValue { + public: + virtual ScriptType type() const { return SCRIPT_COLLECTION; } + virtual String toCode() const; +}; + // Iterator over a collection template class ScriptCollectionIterator : public ScriptIterator { @@ -114,10 +120,9 @@ class ScriptCollectionIterator : public ScriptIterator { /// Script value containing a collection template -class ScriptCollection : public ScriptValue { +class ScriptCollection : public ScriptCollectionBase { public: inline ScriptCollection(const Collection* v) : value(v) {} - virtual ScriptType type() const { return SCRIPT_COLLECTION; } virtual String typeName() const { return _TYPE_1_("collection of", type_name(*value->begin())); } virtual ScriptValueP getIndex(int index) const { if (index >= 0 && index < (int)value->size()) { @@ -190,9 +195,8 @@ class ScriptMap : public ScriptValue { // ----------------------------------------------------------------------------- : Collections : from script /// Script value containing a custom collection, returned from script functions -class ScriptCustomCollection : public ScriptValue { +class ScriptCustomCollection : public ScriptCollectionBase { public: - virtual ScriptType type() const { return SCRIPT_COLLECTION; } virtual String typeName() const { return _TYPE_("collection"); } virtual ScriptValueP getMember(const String& name) const; virtual ScriptValueP getIndex(int index) const; diff --git a/src/script/value.cpp b/src/script/value.cpp index 49c61804..59268fbd 100644 --- a/src/script/value.cpp +++ b/src/script/value.cpp @@ -27,8 +27,9 @@ ScriptValueP ScriptValue::eval(Context&) const { return delay ScriptValueP ScriptValue::next() { throw InternalError(_("Can't convert from ")+typeName()+_(" to iterator")); } ScriptValueP ScriptValue::makeIterator(const ScriptValueP&) const { return delayError(_ERROR_2_("can't convert", typeName(), _TYPE_("collection"))); } int ScriptValue::itemCount() const { throw ScriptError(_ERROR_2_("can't convert", typeName(), _TYPE_("collection"))); } +String ScriptValue::toCode() const { return *this; } CompareWhat ScriptValue::compareAs(String& compare_str, void const*& compare_ptr) const { - compare_str = toString(); + compare_str = toCode(); return COMPARE_AS_STRING; } ScriptValueP ScriptValue::getMember(const String& name) const { @@ -322,12 +323,36 @@ class ScriptNil : public ScriptValue { virtual operator double() const { return 0.0; } virtual operator int() const { return 0; } virtual operator bool() const { return false; } - virtual ScriptValueP eval(Context&) const { return script_nil; } // nil() == nil + virtual ScriptValueP eval(Context& ctx) const { + // nil(input) == input + return ctx.getVariable(SCRIPT_VAR_input); + } }; /// The preallocated nil value ScriptValueP script_nil(new ScriptNil); +// ----------------------------------------------------------------------------- : Collection base + +String ScriptCollectionBase::toCode() const { + String ret = _("["); + bool first = true; + #ifdef USE_INTRUSIVE_PTR + // we can just turn this into a ScriptValueP + // TODO: remove thisP alltogether + ScriptValueP it = makeIterator(ScriptValueP(const_cast((ScriptValue*)this))); + #else + #error "makeIterator needs a ScriptValueP :(" + #endif + while (ScriptValueP v = it->next()) { + if (!first) ret += _(","); + first = false; + ret += v->toCode(); + } + ret += _("]"); + return ret; +} + // ----------------------------------------------------------------------------- : Custom collection // Iterator over a custom collection diff --git a/src/script/value.hpp b/src/script/value.hpp index d1420ff7..7908a279 100644 --- a/src/script/value.hpp +++ b/src/script/value.hpp @@ -67,6 +67,9 @@ class ScriptValue : public IntrusivePtrBaseWithDelete { /// Convert this value to a color virtual operator AColor() const; + /// Script code to generate this value + virtual String toCode() const; + /// Explicit overload to convert to a string /** This is sometimes necessary, because wxString has an int constructor, * which confuses gcc. */ diff --git a/src/util/string.cpp b/src/util/string.cpp index bf1d6773..a85b559a 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -276,7 +276,7 @@ Char decompose_char2(Char c) { } } -bool smart_less(const String& sa, const String& sb) { +int smart_compare(const String& sa, const String& sb) { bool in_num = false; // are we inside a number? bool lt = false; // is sa less than sb? bool eq = true; // so far is everything equal? @@ -295,12 +295,12 @@ bool smart_less(const String& sa, const String& sb) { } } else if (in_num && da) { // comparing numbers, one is longer, therefore it is greater - return false; + return 1; } else if (in_num && db) { - return true; + return -1; } else if (in_num && !eq) { // two numbers of the same length, but not equal - return lt; + return lt ? -1 : 1; } else if (a != b) { // not a number eq = true; lt = false; @@ -310,26 +310,26 @@ bool smart_less(const String& sa, const String& sb) { // Decompose characters Char la2 = decompose_char2(a), lb2 = decompose_char2(b); // Compare - if (la < lb) return true; - if (la > lb) return false; + if (la < lb) return -1; + if (la > lb) return 1; // Remaining from decomposition if (la2 || lb2) { if (la2) a = la2; else { - if (++pa >= na) return false; + if (++pa >= na) return 1; a = sa.GetChar(pa); } if (lb2) b = lb2; else { - if (++pb >= nb) return true; + if (++pb >= nb) return -1; b = sb.GetChar(pb); } goto next; // don't move to the next character in both strings } } else { // control characters - if (a < b) return true; - else return false; + if (a < b) return -1; + else return 1; } } in_num = da && db; @@ -341,16 +341,23 @@ bool smart_less(const String& sa, const String& sb) { if (na - pa < nb - pb) { // number b continues? Char b = sb.GetChar(pb); - if (isDigit(b) || eq) return true; // b is longer + if (isDigit(b) || eq) return -1; // b is longer } else if (na - pa > nb - pb) { Char a = sa.GetChar(pa); - if (isDigit(a) || eq) return false; // a is longer + if (isDigit(a) || eq) return 1; // a is longer } - return lt; // compare numbers + return eq ? 0 : lt ? -1 : 1; // compare numbers } else { - return na - pa < nb - pb; // outside number, shorter string comes first + return na - pa == nb - pb ? 0 + : na - pa < nb - pb ? -1 : 1; // outside number, shorter string comes first } } +bool smart_less(const String& sa, const String& sb) { + return smart_compare(sa, sb) == -1; +} +bool smart_equal(const String& sa, const String& sb) { + return smart_compare(sa, sb) == 0; +} bool starts_with(const String& str, const String& start) { if (str.size() < start.size()) return false; diff --git a/src/util/string.hpp b/src/util/string.hpp index cd2fcf0c..1aec4cad 100644 --- a/src/util/string.hpp +++ b/src/util/string.hpp @@ -162,12 +162,18 @@ String remove_shortcut(const String&); // ----------------------------------------------------------------------------- : Comparing / finding -/// Compare two strings, is the first less than the first? +/// Compare two strings /** Uses a smart comparison algorithm that understands numbers. * The comparison is case insensitive. * Doesn't handle leading zeros. + * + * Returns -1 if a < b, 0 if they are equal, and 1 if a > b */ +int smart_compare(const String&, const String&); +/// Compare two strings, is the first less than the first? bool smart_less(const String&, const String&); +/// Compare two strings for equality +bool smart_equal(const String&, const String&); /// Return whether str starts with start /** starts_with(a,b) == is_substr(a,0,b) */