Unit Price Helper

Automatically generate unit price for applicable items when shopping online.

  1. // ==UserScript==
  2. // @name Unit Price Helper
  3. // @namespace https://github.com/yzhu27/UnitPriceHelper/
  4. // @version 0.1
  5. // @description Automatically generate unit price for applicable items when shopping online.
  6. // @copyright @yzhu27, @Internationale-NCSU, @Lognam-Huang, @MZWANGgg, @IsleZhu
  7. // @match http://www.harristeeter.com/*
  8. // @include http://www.harristeeter.com/*
  9. // @match https://www.harristeeter.com/*
  10. // @include https://www.harristeeter.com/*
  11. // @match http://www.costco.com/*
  12. // @include http://www.costco.com/*
  13. // @match https://www.costco.com/*
  14. // @include https://www.costco.com/*
  15. // @match http://www.target.com/*
  16. // @include http://www.target.com/*
  17. // @match https://www.target.com/*
  18. // @include https://www.target.com/*
  19. // @require https://code.jquery.com/jquery-3.6.1.js
  20. // @grant none
  21. // ==/UserScript==
  22.  
  23.  
  24. const RULE_SET = {
  25. 'https://www.harristeeter.com/p/': {
  26. price_label: "data",
  27. capacity_label: "span[id=ProductDetails-sellBy-unit]",
  28. function: harrisConverter,
  29. label_type: 'value',
  30. append_function: appendForHarris,
  31. website_type: 'static'
  32. },
  33. 'https://www.harristeeter.com/pl/': {
  34. price_label: "data",
  35. capacity_label: "span[class='kds-Text--s text-neutral-more-prominent']",
  36. function: harrisConverter,
  37. label_type: 'value',
  38. append_function: appendForHarris,
  39. website_type: 'static'
  40. },
  41. 'https://www.harristeeter.com/search': {
  42. price_label: "data",
  43. capacity_label: "span[class='kds-Text--s text-neutral-more-prominent']",
  44. function: harrisConverter,
  45. label_type: 'value',
  46. append_function: appendForHarris,
  47. website_type: 'static'
  48. },
  49. 'https://www.costco.com/': {
  50. price_label: 'div[class=price]',
  51. capacity_label: 'span[class=description]',
  52. function: costcoConverter,
  53. label_type: 'text',
  54. append_function: appendForCostco,
  55. website_type: 'static'
  56. },
  57. 'https://www.target.com/s': {
  58. price_label: "span[data-test=current-price]",
  59. capacity_label: "div[class='Truncate-sc-10p6c43-0 dWgRjr']",
  60. function: costcoConverter, // target could share the converter with costco.
  61. label_type: 'text',
  62. append_function: appendForTarget,
  63. website_type: 'dynamic'
  64. },
  65. 'https://www.wholefoodsmarket.com/search': {
  66. price_label: "span[class=regular_price]",
  67. capacity_label: "h2[data-testid=product-tile-name]",
  68. //function: targetConverter,
  69. label_type: 'text',
  70. append_function: appendForTarget,
  71. website_type: 'dynamic'
  72. }
  73. };
  74. const TARGET_URL_PREFIX = [
  75. 'https://www.harristeeter.com/p/',
  76. 'https://www.harristeeter.com/search',
  77. 'https://www.costco.com/',
  78. 'https://www.target.com/s',
  79. 'https://www.wholefoodsmarket.com/search',
  80. 'https://www.harristeeter.com/pl/'
  81. ];
  82. const REGEX = {
  83. unit : 'gal|g|kg|lbs|lb|fl oz|oz|qt|fl. oz|ml|litter|Litter|l|L',
  84. quant : 'ct|pack|count',
  85. float : "\\d+\\.?\\d*?(?:\\s*-\\s*\\d+\\.?\\d*?)?"
  86. };
  87. (function () {
  88. var host = window.location.host.toLowerCase();
  89. window.priceTipEnabled = true;
  90. console.log(host)
  91. var url = window.location.href.toLowerCase();
  92. for (let i = 0; i < TARGET_URL_PREFIX.length; i++) {
  93. if (url.startsWith(TARGET_URL_PREFIX[i])) {
  94. if (RULE_SET[TARGET_URL_PREFIX[i]].website_type == 'static') {
  95. addListPriceTips(TARGET_URL_PREFIX[i])
  96. } else if (RULE_SET[TARGET_URL_PREFIX[i]].website_type == 'dynamic') {
  97. window.addEventListener("wheel", event => {
  98. addListPriceTips(TARGET_URL_PREFIX[i])
  99. })
  100. }
  101. }
  102. }
  103. })();
  104. /**
  105. * @property {Function}addListPriceTips Acts like an controller. Designated different converter
  106. * and append function for 'addTipsHelper()' according to 'url_prefix'
  107. * @param {string} url_prefix the unique identifier prefix of target url.
  108. * @returns no return value
  109. */
  110. function addListPriceTips(url_prefix) {
  111. console.log('addListPriceTips_ is called:' + url_prefix);
  112. // query didn't work for 'Target' website
  113. //console.log(document);
  114. var totalPrice = document.querySelectorAll(RULE_SET[url_prefix].price_label);
  115. var totalVolumn = document.querySelectorAll(RULE_SET[url_prefix].capacity_label);
  116. //console.log(RULE_SET[url_prefix].price_label);
  117. //console.log('len: '+totalPrice.length);
  118. //console.log('price: ' + totalPrice[0].textContent);
  119. //console.log('volume: ', totalVolumn[0].textContent);
  120.  
  121. var labelType = RULE_SET[url_prefix].label_type;
  122. var len = totalPrice.length;
  123.  
  124. for (let i = 0; i < len; i++) {
  125. if (totalPrice[i] === null || totalVolumn[i] === null) {
  126. continue;
  127. }
  128. if (labelType === 'value') {
  129. addTipsHelper(totalPrice[i].value, totalVolumn[i].textContent, RULE_SET[url_prefix].function, RULE_SET[url_prefix].append_function, i);
  130. } else if (labelType === 'text') {
  131. addTipsHelper(totalPrice[i].textContent, totalVolumn[i].textContent, RULE_SET[url_prefix].function, RULE_SET[url_prefix].append_function, i);
  132. }
  133. }
  134. console.log(len);
  135. return len;
  136. }
  137. /**
  138. * @property {Function}addTipsHelper Acts like an executor. Finished the unit calculating and converting according to the given params.
  139. * @param {string} price the raw price value extracted from labels
  140. * @param {string} title the raw title value extracted from labels. Volumn is matched using regex from title.
  141. * @returns no return value
  142. */
  143. function addTipsHelper(price, title, func, appendFun, index) {
  144. var convertedResult = func(price, title);
  145. if (convertedResult != null) {
  146. console.log(convertedResult.finalPrice + '/' + convertedResult.finalUnit);
  147. appendFun(convertedResult, index);
  148. }
  149.  
  150. }
  151. function appendForCostco(convertedResult, index) {
  152. console.log('unit price:' + convertedResult.finalPrice, 'unit: ' + convertedResult.finalUnit);
  153. var priceSpan = "[" + convertedResult.finalPrice + " / " + convertedResult.finalUnit + "]";
  154. document.getElementsByClassName('price')[index].append(priceSpan);
  155. }
  156. function appendForHarris(convertedResult, index) {
  157. var priceSpan = document.createElement('span');
  158. priceSpan.innerHTML = "[" + convertedResult.finalPrice + " / " + convertedResult.finalUnit + "]";
  159. priceSpan.className = 'kds-Price-promotional-dropCaps';
  160. //left border/margin fails to work
  161. priceSpan.style = "font-size: 16px; left-margin: 20px";
  162.  
  163. //following line is originally working
  164. //document.getElementsByClassName('kds-Price-promotional kds-Price-promotional--decorated')[index].appendChild(priceSpan);
  165.  
  166. //following is trying to solve discount item issue, still require testing
  167. //use the length of testResult to check whether the price/unit is already provided by the website
  168. //if it is provided, the length should be 1 - only has finalPrice as the result
  169. //otherwise, the length is 2 - finalPrice and finalUnit
  170. if (Object.keys(convertedResult).length == 2) {
  171. priceSpan.innerHTML = "[$" + convertedResult.finalPrice + " / " + convertedResult.finalUnit + "]";
  172.  
  173. priceSpan.className = 'kds-Price-promotional-dropCaps';
  174. //left border/margin fails to work
  175. priceSpan.style = "font-size: 16px; left-margin: 20px";
  176.  
  177. //not elegant, but works
  178. //use the length of class name to determine whether the item is having discount
  179. //if the item is having discount, the length should be 54
  180. //if the item is not having discount, the length should be 83
  181. var insertedTag = document.getElementsByClassName('kds-Price-promotional kds-Price-promotional--decorated')[index];
  182. if (insertedTag.className.length == 54) {
  183. insertedTag.appendChild(priceSpan);
  184. } else if (insertedTag.className.length == 83) {
  185. insertedTag.appendChild(priceSpan);
  186. } else {
  187. alert("ERROR: not tag to insert span");
  188. }
  189.  
  190.  
  191. //try to change CSS, this need to use append(), but failed because is regarded as string
  192. // var priceSpan = "<span class=\"kds-Price-promotional-dropCaps\">"+testResult.finalPrice+" / "+testResult.finalUnit+"</span>";
  193. // document.getElementsByClassName('kds-Price-promotional kds-Price-promotional--plain kds-Price-promotional--decorated')[0].append(priceSpan);
  194.  
  195. } else {
  196. console.log("Price/unit is already provided.")
  197. }
  198. }
  199. function appendForTarget(convertedResult, index) {
  200. var priceSpan = "[" + convertedResult.finalPrice + " / " + convertedResult.finalUnit + "]";
  201. if (!document.querySelectorAll('span[data-test=current-price]')[index].textContent.endsWith('.')) {
  202. document.querySelectorAll('span[data-test=current-price]')[index].append(priceSpan + '.');
  203. }
  204. }
  205. function harrisConverter(price, title) {
  206. //solve if the price/unit is already provided by the website
  207. var itemFinalUnit = '';
  208. if (title[0] == '$') {
  209. itemFinalUnit = title;
  210. return {
  211. finalPrice: itemFinalUnit
  212. }
  213. } else {
  214. //quantity cannot solve 1/2 yet
  215. //quantity can already solve 0.5 by yZhu
  216. var itemQuantity = title.match(/([1-9]\d*\.?\d*)|(0\.\d*[1-9])/)[0];
  217. //console.log(itemQuantity);
  218.  
  219. //optimize to solve special cases as '20 ct 0.85'
  220. var itemUnit = title.match(/\s((([a-zA-Z]*\s?[a-zA-Z]+)*))/)[1];
  221. //console.log(itemUnit);
  222. var itemPriceByUnit = parseFloat(price) / parseFloat(itemQuantity);
  223. //cut long tails after digit
  224. itemPriceByUnit = itemPriceByUnit.toFixed(3);
  225. //console.log(itemPriceByUnit);
  226.  
  227.  
  228. switch (itemUnit) {
  229. case 'gal': itemFinalUnit = 'gal';
  230. break;
  231. case 'oz': itemFinalUnit = 'oz';
  232. break;
  233. case 'fl oz': itemFinalUnit = 'oz';
  234. break;
  235. case 'ct': itemFinalUnit = 'count';
  236. break;
  237. case 'lb': itemFinalUnit = 'lb';
  238. break;
  239. case 'pack': itemFinalUnit = 'pack';
  240. break;
  241. case 'pk': itemFinalUnit = 'pack';
  242. break;
  243. case 'bottles': itemFinalUnit = 'Bottle';
  244. break;
  245. case 'cans': itemFinalUnit = 'can';
  246. break;
  247. case 'L': itemFinalUnit = 'L';
  248. break;
  249. case 'l': itemFinalUnit = 'L';
  250. break;
  251. case 'ml': itemFinalUnit = 'ml';
  252. break;
  253. case 'unit': itemFinalUnit = 'unit';
  254. break;
  255. case 'box': itemFinalUnit = 'box';
  256. break;
  257. case 'boxes': itemFinalUnit = 'box';
  258. break;
  259. case 'suit': itemFinalUnit = 'suit';
  260. break;
  261. case 'suits': itemFinalUnit = 'suit';
  262. break;
  263. case 'bag': itemFinalUnit = 'bag';
  264. break;
  265. case 'bags': itemFinalUnit = 'bag';
  266. break;
  267. //may be some other units else?
  268.  
  269. default: itemFinalUnit = 'unknown unit';
  270. }
  271.  
  272. if (itemPriceByUnit > 1000 || itemPriceByUnit < 0) {
  273. return null;
  274. }
  275. else {
  276. //console.log("Hihi");
  277. return {
  278. finalPrice: itemPriceByUnit,
  279. finalUnit: itemFinalUnit
  280. };
  281. }
  282. }
  283. }
  284.  
  285. function costcoConverter(price, title) {
  286. title = title.trim().toLowerCase();
  287. console.log('title: ' + title);
  288. price = parseFloat(price.trim().substring(1));
  289. var regQuant = REGEX.quant;
  290. var regUnit = REGEX.unit;
  291. var regFloat = REGEX.float;
  292. var unitMatcher = new RegExp('(' + regFloat + ')-?\\s*?' + '('+ regUnit + ')');
  293. var quantMatcher = new RegExp('(' + regFloat + ')-?\\s*?' + '('+ regQuant + ')');
  294. var matchQuant = null;
  295. var matchUnit = null;
  296. matchQuant = quantMatcher.exec(title);
  297. matchUnit = unitMatcher.exec(title);
  298. var unit = ' ';
  299. var count = 'count';
  300. var quant = 1;
  301. var capacity = 1;
  302. if(matchQuant!=null){
  303. console.log(matchQuant[0]);
  304. count = matchQuant[2];
  305. quant = parseFloat(matchQuant[1]);
  306. }
  307. if(matchUnit!=null){
  308. console.log(matchUnit[0]);
  309. unit = matchUnit[2];
  310. capacity = parseFloat(matchUnit[1]);
  311. }
  312. var unitPrice = parseFloat(price) / (capacity*quant);
  313. if(unit === ' ' ){
  314. unit = count;
  315. }
  316. return {
  317. finalUnit: unit,
  318. finalPrice: Math.round(unitPrice * 100) / 100,
  319. };
  320.  
  321. }
  322.  
  323. if (typeof process === "object" && typeof require === "function") {
  324. module.exports={
  325. addListPriceTips,
  326. harrisConverter,
  327. costcoConverter,
  328. appendForHarris,
  329. appendForCostco,
  330. appendForTarget
  331. };
  332. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址