How to format money on Shopify using JavaScript - shopify

I found this Gist for money formatting on Shopify using JavaScript https://gist.github.com/stewartknapman/8d8733ea58d2314c373e94114472d44c
I placed it in my cart page and when I try:
Shopify.formatMoney(2000, '$')
I get this:
cart:2166 Uncaught TypeError: Cannot read property '1' of null
at Object.Shopify.formatMoney (cart:2166)
at <anonymous>:1:9
this is at switch(formatString.match(placeholderRegex)[1]) {
I expect to get $20.00
Do you know where the problem is?
Similar question: Shopify Buy Button Error: "cannot read property '1' of null"
The Gist content
var Shopify = Shopify || {};
// ---------------------------------------------------------------------------
// Money format handler
// ---------------------------------------------------------------------------
Shopify.money_format = "${{amount}}";
Shopify.formatMoney = function(cents, format) {
if (typeof cents == 'string') { cents = cents.replace('.',''); }
var value = '';
var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/;
var formatString = (format || this.money_format);
function defaultOption(opt, def) {
return (typeof opt == 'undefined' ? def : opt);
}
function formatWithDelimiters(number, precision, thousands, decimal) {
precision = defaultOption(precision, 2);
thousands = defaultOption(thousands, ',');
decimal = defaultOption(decimal, '.');
if (isNaN(number) || number == null) { return 0; }
number = (number/100.0).toFixed(precision);
var parts = number.split('.'),
dollars = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands),
cents = parts[1] ? (decimal + parts[1]) : '';
return dollars + cents;
}
switch(formatString.match(placeholderRegex)[1]) {
case 'amount':
value = formatWithDelimiters(cents, 2);
break;
case 'amount_no_decimals':
value = formatWithDelimiters(cents, 0);
break;
case 'amount_with_comma_separator':
value = formatWithDelimiters(cents, 2, '.', ',');
break;
case 'amount_no_decimals_with_comma_separator':
value = formatWithDelimiters(cents, 0, '.', ',');
break;
}
return formatString.replace(placeholderRegex, value);
};

I found formatMoney function in my theme and I was able to call it and it worked.
pipelineVendor.themeCurrency.formatMoney(2000);
result: $20.00

It's probably too late for the original asker, but for anyone else looking for an answer, I believe the correct call for this function is:
Shopify.formatMoney(2000, '${{amount}}')
instead of
Shopify.formatMoney(2000, '$')
based on the comment of the gist author on Github:
It doesn’t actually care what the currency is. The first argument is
the unformatted amount. The second argument is the format, which could
be whatever you need for the currently displayed currency i.e. “£{{
amount }}”. Then pass it a new amount and new format when you change
the currency.

Related

Lua implementation of BestSum function using memoization

I am trying to translate the below javascript "bestSum" memoization function into lua:
const bestSum = (targetSum,numbers,memo ={}) => {
if(targetSum in memo) return memo[targetSum];
if(targetSum === 0 ) return [];
if(targetSum <0)return null;
let shortestCombination = null;
for (let num of numbers) {
const remainder = targetSum - num;
const remainderCombination = bestSum(remainder,numbers,memo);
if (remainderCombination !==null) {
const combination = [...remainderCombination, num];
if (shortestCombination === null || combination.length < shortestCombination.length)
{
shortestCombination = combination;
}
}
}
memo [targetSum] = shortestCombination;
return shortestCombination;
}
sample test cases with correct results:
console.log(bestSum(7,[5,3,4,7])); //[7]
console.log(bestSum(8,[2,3,5])); //[3,5]
console.log(bestSum(8,[1,4,5])); //[4,4]
console.log(bestSum(100,[1,2,5,25])); //[25,25,25,25]
I translated the above javascript into lua as the following:
local function BestSum(target_sum,numbers,memo)
if memo[target_sum] ~= nil then return memo[target_sum] end
if target_sum == 0 then return {} end
if target_sum < 0 then return nil end
local shortest_combination = nil
for i, num in ipairs (numbers) do
local remainder = target_sum - num
local remainder_combination = BestSum(remainder,numbers, memo)
if remainder_combination ~= nil then
local combination = remainder_combination
table.insert(combination,num )
if (shortest_combination == nil) or (#combination < #shortest_combination )then
shortest_combination = combination
end
end
end
memo[target_sum] = shortest_combination;
return shortest_combination;
end
but don't get the desired results for the two last cases...... instead get incorrect results:
BestSum(8,{1,4,5},{})==>{"4","1","4"}
BestSum(150,{5,25},{})==>
{"25","5","5","5","5","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25","5","25"}
The results are not even correct let alone being "best" case??
Can anyone spot where I'm going wrong?
Much appreciated
The problem is with this part of the translation:
local combination = remainder_combination
table.insert(combination, num)
Tables are pass by reference, so this isn't creating a new table, it's just assigning the variable combination to the same table. Modifying combination is just adding more data to remainder_combination.
The JavaScript version is taking care to create a new array, and fills it with the contents of the remainderCombination array (using '...', the spread operator):
const combination = [...remainderCombination, num];
This is the most accurate Lua translation:
local combination = {unpack(remainder_combination)}
table.insert(combination, num)
(Edit: For Lua 5.2+ it's table.unpack)

xpages currency incomplete entry failing

I have a form that accepts currency from several fields and depending on what I enter into one, it calculates others. For this most part the code below works (example being the invoice amount field)
<xp:inputText value="#{FInvoiceDoc.InvoiceAmount}" id="InvoiceAmount">
<xp:this.converter>
<xp:convertNumber type="currency"></xp:convertNumber></xp:this.converter>
<xp:eventHandler event="onchange" submit="false" id="eventHandler5">
<xp:this.script><![CDATA[var rate = XSP.getElementById("#{id:ExchangeRate}").value;
var stAmount = XSP.getElementById("#{id:InvoiceAmount}").value;
var stTvat = XSP.getElementById("#{id:TVAT}").value;
var stTst = XSP.getElementById("#{id:TST}").value;
var stToop = XSP.getElementById("#{id:Toop}").value;
var tmp;
// get the dojo currency code
dojo.require("dojo.currency");
// get the numeric values using parse
amount = dojo.currency.parse(stAmount,{currency:"USD"});
total = rate * amount;
XSP.getElementById("#{id:EstUSAmount}").innerHTML = dojo.currency.format(total,{currency:"USD"});
XSP.getElementById("#{id:InvoiceAmount}").value = dojo.currency.format(amount,{currency:"USD"});
if (amount != 0) {
tvat = dojo.currency.parse(stTvat,{currency:"USD"});
tst = dojo.currency.parse(stTst,{currency:"USD"});
toop = dojo.currency.parse(stToop,{currency:"USD"});
tmp = (tvat / (amount-tvat-tst-toop)) * 100;
XSP.getElementById("#{id:VP}").innerHTML = tmp.toFixed(2);
tmp = (tst / (amount-tvat-tst-toop)) * 100;
XSP.getElementById("#{id:STP}").innerHTML = tmp.toFixed(2);
tmp = (toop / (amount-tvat-tst-toop)) * 100;
XSP.getElementById("#{id:OoPP}").innerHTML = tmp.toFixed(2);
}
]]></xp:this.script>
</xp:eventHandler>
</xp:inputText>
Like I said, for the most part this works, but if I enter only a partial number like
200.2
instead of
200.20
then it fails. Most data entry folks will want to not have to key in the last "0" just so it's legal.
btw, if I enter it in as just 200 with no cents, it's OK.
It's as if the statement;
amount = dojo.currency.parse(stAmount,{currency:"USD"});
requires a 2 digit penny amount or no pennys, but doesn't like just the leading cents digit.
Any way around this?
The currency format is very restrictive and needs the certain number of decimal places for a given currency. As you can see here there can be different number of decimal places depending on currency. For most currencies it needs two decimal places though. But, you can trick the parser. Just try it with standard number of decimal places and if it fails try it with one decimal place again. Your code would look like this then:
var amount = dojo.currency.parse(stAmount,{currency:"USD"});
if (!amount && amount!==0) {
amount = dojo.currency.parse(stAmount,{currency:"USD",places:"1"});
}
This accepts inputs like
12
12.3
12.34
$12
$12.3
$12.34
But it is still very picky. It doesn't accept spaces or more decimal places then currency allows.
If you want more flexibility for your users and need USD currency only I'd go for dojo.number or other number parsing instead and show the "$" outside the input field. Then, you'd be able to accept much more formats and could add functions like rounding.
Yeah, I've played with the converters. Thing is I want the converter to stay "Currency". The data I receive from the notes database is always correct and formats properly. Its the manipulation that I''m having a problem with. Since this is all on the client side, I've created a function that I can reuse throughout the page.
<script>
function isNumeric(n) {
n = parseFloat(n);
return !isNaN(n) || n != n;
}
// We call this if its a currency amount. If they only entered one number after
// the decimal, then we just append a 0. We're expecting a string and we send
// the string back.
function cMask(Amount)
{
if (Amount == "") { return "0.00" }
a = Amount.split(".");
if (a.length == 2)
{
if (a[1] != null)
{
if (a[1].length == 1)
{
Amount = Amount + "0";
}
}
}
// get the dojo currency code
dojo.require("dojo.currency");
b = dojo.currency.parse(Amount,{currency:"USD"});
if (isNumeric(b)) {return Amount} else {return "0.00"}
}
</script>
Then its a matter of just changing the initial variable load line.
var stAmount = cMask(XSP.getElementById("#{id:InvoiceAmount}").value);
Seems to work and I've just taught myself how to create reusable client-side javascript for my page.

KnockoutJS throttle input

I'm trying to implement something like a typesafe ViewModel using KnockoutJS. It works pretty well until I start to update observables via HTML input tags.
I have implemented type extender which returns computed observable:
return ko.computed({
read: target,
write: fixer
})
Where fixer is something like:
function (newValue) {
var current = target(),
valueToWrite = (newValue == null ? null : fixNumber(newValue, 0));
if (valueToWrite !== current) target(valueToWrite);
else if (newValue !== current) target.notifySubscribers(valueToWrite);
}
And fixNumber is
function fixNumber(value, precision) {
if (value == null || value === '') return null;
var newValue = (value || '').toString().replace(/^[^\,\.\d\-]*([\.\,\-]?\d*)([\,\.]?\d*).*$/, '$1$2').replace(/\,/, '.'),
valueToWrite = Number(newValue);
return !!(valueToWrite % 1) ? round(valueToWrite, precision) : valueToWrite;
}
It looks not so straightforward, but I have to consider possible use of comma as a decimal separator.
Often I need to update my observables as soon as user presses key to reflect this change immediately:
<input type="text" data-bind="value: nonThrottled, valueUpdate: 'afterkeyup'"></input>
And here comes a lot of problems, because, for example, I can't input decimal values less than 1 (0.1, 0.2, etc) there.
When I try to throttle an observable it mostly works. But sometimes user input and type fixer go out of sync, so it looks like some input gets lost occasionally.
Full example is there http://jsfiddle.net/mailgpa/JHztW/. I would really appreciate any hints, since I have spent days trying to fix these problems.
UPDATE 11/04/2013
I solved my problem providing custom value binding, so now throttled observables doesn't eat my input occasionally.
I've added additional valueThrottle option-binding to throttle updating of element's value:
var valueThrottle = allBindingsAccessor()["valueThrottle"];
var valueThrottleTimeoutInstance = null;
/* ... */
if (valueThrottle) {
clearTimeout(valueThrottleTimeoutInstance);
valueThrottleTimeoutInstance = setTimeout(function () {
ko.selectExtensions.writeValue( element, ko.utils.unwrapObservable(valueAccessor()) );
}, valueThrottle);
} else applyValueAction();
Also I've noticed that inability to enter values like 0.2 in my case comes from that statement in original value binding:
if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
valueHasChanged = true;
I've rewritten it as
if ((newValue === 0) && (elementValue != 0))
valueHasChanged = true;
It works at least at Chrome, but I haven't tested it properly and even not sure that it's correct.
Example is to be added, for some reason jsFiddle does not accept my custom binding.
Any comments are really appreciated.

Formatted date filter in CGridView

I display my date in CGridView as: "22.6.2012 22:53" with:
array('name' => 'date',
'value' => date("j.n.Y G:i", strtotime($model->date))
),
But in my filter, I need to search in this format (which is in the database) to get results: "2012-06-22 22:53".
How can I make my filter to work in the format that is displayed in my CGridView? I've searched for an answer but haven't found one, I've also tried adding the date function in my model search() for this attribute:
$criteria->compare('date', date("j.n.Y G:i", strtotime($this->date), true);
but then I just get an empty list :)
Help would be greatly appreciated.
To begin with, you should not be using the value property to control the formatting of dates. The proper way is to set the type property to 'date' and, if you do not do this already, set CApplication.language to target the appropriate locale.
For the filter it would be best for the user if you use a CJuiDatePicker widget to let the user visually pick the date; there's a short and to-the-point guide on how to do that here.
Update:
Formatting columns with type == 'date' is done through CGridView.formatter, for which if you do not explicitly set a value the default is whatever the 'format' application component is. So you can specify and configure a CFormatter on the spot, or if you want to use the application's formatter but with slight modifications you can do
$formatter = clone Yii::app()->format;
$formatter->dateFormat = 'whatever'; // or $formatter->dateTimeFormat
and then assign this instance to CGridView.formatter.
compare() makes a sql sentence with the input, so I had to change the input to my wanted format.
my function:
function changeDateToDBformat($datum) {
if (strstr($datum, '.') || strstr($datum, ':')) {
$formats = array('!j.n', '!j.n.Y', '!j.n.Y H:i', '!n.Y H:i', '!n.Y', '!H:i', '!j.n.Y H', '!n.Y H', '!Y H:i', '!Y H');
$date = false;
foreach ($formats as $format) {
$date = DateTime::createFromFormat($format, $datum);
if (!($date === false)) {
$izbraniFormat = $format;
break;
}
}
if (!$date === false) {
$datum1 = $date->format('Y-m-d H:i');
$date2 = DateTime::createFromFormat(substr($izbraniFormat, 1, strlen($izbraniFormat)), $datum);
$datum2 = $date2->format('Y-m-d H:i');
$datumcas1 = explode(' ', $datum1);
$datumcas2 = explode(' ', $datum2);
$prvidatum = explode('-', $datumcas1[0]);
$drugidatum = explode('-', $datumcas2[0]);
$koncniDatum = '';
for ($a = 0; $a < sizeof($prvidatum); $a++) {
if ($prvidatum[$a] == $drugidatum[$a])
$koncniDatum .= '-' . $prvidatum[$a];
}
$koncniCas = '';
$prvicas = explode('-', $datumcas1[1]);
$drugicas = explode('-', $datumcas2[1]);
for ($a = 0; $a < sizeof($prvicas); $a++) {
if ($prvicas[$a] == $drugicas[$a])
$koncniCas .= ':' . $prvicas[$a];
}
$koncniDatum = substr($koncniDatum, 1, strlen($koncniDatum));
if (strlen($koncniCas) > 0)
$koncniDatum .= ' ' . substr($koncniCas, 1, strlen($koncniCas));
$datum = $koncniDatum;
}
}
return $datum;
}
//translations:
//izbrani == selected
//datum == date
//cas == time
//koncni == end
//prvi == first
//drugi == second
With this, a user can enter date in the format "j.n.Y H:i" and also just portions of this format (j.n, n.Y, Y H:i,...).
I would like to thank Jon and nickb for help! link
Like many others I also struggled with this, well displaying the grid wasn't the problem, but filtering in the localized datetime was!
So I created my own formatter, used it in the search() function of my models (when passing the search parameters to compare()) and it works like a charm.
I can now filter on date/datetime fields in any localization (I use Dutch):
"30-12-2018" becomes "2018-12-30"
">30-12-2018" becomes ">2018-12-30"
"30-12-2018 23:59:49" becomes "2018-12-30 23:59:49"
">=30-12-2018 23:59:49" becomes ">=2018-12-30 23:59:49"
My localization:
// dateFormat['short'] = 'dd-MM-yyyy'
// timeFormat['medium'] = 'HH:mm:ss'
Yii::app()->format->datetimeFormat = strtr(Yii::app()->locale->dateTimeFormat,
array("{0}" => Yii::app()->locale->getTimeFormat('medium'),
"{1}" => Yii::app()->locale->getDateFormat('short')));
Yii::app()->format->dateFormat = 'short';
Yii::app()->format->timeFormat = 'medium';
My CGridView contains the following date time column:
'mutation_date_time:dateTime'
And (a snippet of) my own formatter with some handy functions:
class Formatter extends CLocalizedFormatter
{
public function formatWithoutSearchOperator($value)
{
// This snippet is taken from CDbCriteria->compare()
if(preg_match('/^(?:\s*(<>|<=|>=|<|>|=))?(.*)$/',$value,$matches))
{
$value=$matches[2];
$op=$matches[1];
}
else
$op='';
return $value;
}
public function formatOnlySearchOperator($value)
{
// This snippet is taken from CDbCriteria->compare()
if(preg_match('/^(?:\s*(<>|<=|>=|<|>|=))?(.*)$/',$value,$matches))
{
$value=$matches[2];
$op=$matches[1];
}
else
$op='';
return $op;
}
/*
* Format a localized datetime back to a database datetime (Y-m-d H:i:s).
* If a comparison operator is given, it is preserved. So strip it if you need to save the date in the database.
* If no time given, it's also not returned (MySQL database appends '00:00:00' as time to it upon saving).
* With this function the following localized datetimes just work like the stock datetime filters:
* - "30-12-2018" becomes "2018-12-30"
* - "30-12-2018 " becomes "1970-01-01" (note the extra space in input)
* - ">30-12-2018" becomes ">2018-12-30"
* - "30-12-2018 23:59:49" becomes "2018-12-30 23:59:49"
* - ">=30-12-2018 23:59:49" becomes ">=2018-12-30 23:59:49"
*
* For save() and afterFind() integration see:
* https://github.com/YetOpen/i18n-datetime-behavior
*/
public function formatToDatabaseDatetime($value)
{
// get the comparison operator from the string:
$comparator = $this->onlySearchOperator($value);
// get the datetime without the comparison operator:
$datetime = $this->withoutSearchOperator($value);
// parse the given datetime according to the locale format to a timestamp
$datetime_parsed = CDateTimeParser::parse(
$datetime,
strtr(
Yii::app()->locale->datetimeFormat,
array(
"{0}" => Yii::app()->locale->getTimeFormat(Yii::app()->format->timeFormat),
"{1}" => Yii::app()->locale->getDateFormat(Yii::app()->format->dateFormat)
)
)
);
// if its not a valid date AND time, check if it can be parsed to a date only:
if($datetime_parsed === false)
{
$date_parsed = CDateTimeParser::parse(
$datetime,
Yii::app()->locale->getDateFormat(Yii::app()->format->dateFormat)
);
}
// If no time part given, also output only the date
if($datetime_parsed===false)
{
$transformed = date(
'Y-m-d',
$date_parsed
);
}
else
{
$transformed = date(
'Y-m-d H:i:s',
$datetime_parsed
);
}
return $comparator . $transformed;
}
}
And within my search() function in my CActiveRecord model I use the following to compare the localized datetime with the records in the database:
$criteria->compare('mutation_date_time',Yii::app()->format->toDatabaseDateTime(trim($this->mutation_date_time)),true);
Please note the trim() there, that's by design (see function description formatToDatabaseDateTime()).
A big difference with filtering directly in correct database format: an invalid date converts to "1970-01-01"!
I highly appreciate feedback and I really hope my code helps somebody!

websql use select in to get rows from an array

in websql we can request a certain row like this:
tx.executeSql('SELECT * FROM tblSettings where id = ?', [id], function(tx, rs){
// do stuff with the resultset.
},
function errorHandler(tx, e){
// do something upon error.
console.warn('SQL Error: ', e);
});
however, I know regular SQL and figured i should be able to request
var arr = [1, 2, 3];
tx.executeSql('SELECT * FROM tblSettings where id in (?)', [arr], function(tx, rs){
// do stuff with the resultset.
},
function errorHandler(tx, e){
// do something upon error.
console.warn('SQL Error: ', e);
});
but that gives us no results, the result is always empty. if i would remove the [arr] into arr, then the sql would get a variable amount of parameters, so i figured it should be [arr]. otherwise it would require us to add a dynamic amount of question marks (as many as there are id's in the array).
so can anyone see what i'm doing wrong?
aparently, there is no other solution, than to manually add a question mark for every item in your array.
this is actually in the specs on w3.org
var q = "";
for each (var i in labels)
q += (q == "" ? "" : ", ") + "?";
// later to be used as such:
t.executeSql('SELECT id FROM docs WHERE label IN (' + q + ')', labels, function (t, d) {
// do stuff with result...
});
more info here: http://www.w3.org/TR/webdatabase/#introduction (at the end of the introduction)
however, at the moment i created a helper function that creates such a string for me
might be better than the above, might not, i haven't done any performance testing.
this is what i use now
var createParamString = function(arr){
return _(arr).map(function(){ return "?"; }).join(',');
}
// when called like this:
createparamString([1,2,3,4,5]); // >> returns ?,?,?,?,?
this however makes use of the underscore.js library we have in our project.
Good answer. It was interesting to read an explanation in the official documentation.
I see this question was answered in 2012. I tried it in Google 37 exactly as it is recommened and this is what I got.
Data on input: (I outlined them with the black pencil)
Chrome complains:
So it accepts as many question signs as many input parameters are given. (Let us pay attention that although array is passed it's treated as one parameter)
Eventually I came up to this solution:
var activeItemIds = [1,2,3];
var q = "";
for (var i=0; i< activeItemIds.length; i++) {
q += '"' + activeItemIds[i] + '", ';
}
q= q.substring(0, q.length - 2);
var query = 'SELECT "id" FROM "products" WHERE "id" IN (' + q + ')';
_db.transaction(function (tx) {
tx.executeSql(query, [], function (tx, results1) {
console.log(results1);
debugger;
}, function (a, b) {
console.warn(a);
console.warn(b);
})
})