I have a basic shopping cart. I thought it was working but I have since discovered that even though it runs error free it is not doing one thing I thought it should and I am having trouble resolving the issue.
If I instantiate an item a second time, like this:
item1 = Item(1, "Cucumbers", 1., 1, 'kg')
item2 = Item(2, "Tissues", 2., 2, 'dozen')
item3 = Item(3, "Tomatoes", 3., 5, 'pound')
# item4 = Item(4, "Toothpaste", 1., 5, 'box')
item4 = Item(4, "Cucumbers", 1., 2, 'kg')
What I want is for the quantity of cucumbers in item 1 to increase to '3'
and for item 4 to be auto-deleted.
I thought this was doing what I intended, but it's not:
elif k == 'name':
total_qty = v.qty + item.qty
if total_qty:
v.qty = total_qty
continue
self.remove_item(k)
else:
v[k] = item[k]
The code runs error free but I end up with two separate instances of cucumber.
Full code:
class Item(object):
def __init__(self, unq_id, name, price, qty, measure):
self.unq_id = unq_id
self.product_name = name
self.price = price
self.qty = qty
self.measure = measure
class Cart(object):
def __init__(self):
self.content = dict()
def __format__(self, format_type):
if format_type == 'short':
return ', '.join(item.product_name for item in self.content.values())
elif format_type == 'long':
return '\n'.join(f'\t\t{item.qty:2} {item.measure:7} {item.product_name:12} # '
f'${item.price:1.2f} ... ${item.qty * item.price:1.2f}'
for item in self.content.values())
def add(self, item):
if item.unq_id not in self.content:
self.content.update({item.unq_id: item})
return
for k, v in self.content.get(item.unq_id).items():
if k == 'unq_id':
continue
elif k == 'name':
total_qty = v.qty + item.qty
if total_qty:
v.qty = total_qty
continue
self.remove_item(k)
else:
v[k] = item[k]
def get_total(self):
return sum([v.price * v.qty for _, v in self.content.items()])
def get_num_items(self):
return sum([v.qty for _, v in self.content.items()])
def remove_item(self, key):
self.content.pop(key)
if __name__ == '__main__':
item1 = Item(1, "Cucumbers", 1., 1, 'kg')
item2 = Item(2, "Tissues", 2., 2, 'dozen')
item3 = Item(3, "Tomatoes", 3., 5, 'pound')
# item4 = Item(4, "Toothpaste", 1., 5, 'box')
item4 = Item(4, "Cucumbers", 1., 2, 'kg')
cart = Cart()
cart.add(item1)
cart.add(item2)
cart.add(item3)
cart.add(item4)
print("Your cart contains: {0:short}".format(cart))
# cart.remove_item(1)
print()
print("Your cart contains: \n {0:long}".format(cart))
print()
print("The total number of items in your cart is: ", cart.get_num_items())
print()
print("The total cost of the items in your cart is: ", cart.get_total())
print()
cart.remove_item(3)
print("Your cart contains: {0:short}".format(cart))
print()
print("Your cart contains: \n {0:long}".format(cart))
print()
print("The total number of items in your cart is: ", cart.get_num_items())
print()
print("The total cost of the items in your cart is: ", cart.get_total())
The fix is:
def add(self, item):
if item.unq_id not in self.content:
self.content.update({item.unq_id: item})
return
else:
self.content[item.unq_id].qty = self.content[item.unq_id].qty + item.qty
Related
The code doesn't output what I expected. How do I get the code to output when i call the function?
import calendar
import datetime
from datetime import date
Eke, Orie, Afor, Nkwo = 0, 1, 2, 3
start_year = 1600
def IgboWeek(func):
def wrapper_func():
if date.year != start_year:
Eke = calendar.SUNDAY
Orie = calendar.MONDAY
Afor = calendar.TUESDAY
Nkwo = calendar.WEDNESDAY
calendar.THURSDAY = Eke
calendar.FRIDAY = Orie
calendar.SATURDAY = Afor
func()
return wrapper_func
#IgboWeek
def birthday():
birthday = input("Enter your birthday in the format: YYYY/MM/DD: ")
if Eke in birthday:
print("Eke")
elif Orie in birthday:
print("Orie")
elif Afor in birthday:
print("Afor")
elif Nkwo in birthday:
print("Nkwo")
else:
print("Not in a good mood for that")
Igobirthday = IgboWeek(birthday)
Igobirthday()
Good day:
would like to ask a simple question regarding my code on Python 3.6
I want to count all the characters that repeats itself
here is the sample of the code that i worked on:
name01 = input("1st Name: ")
ch_name01 = []
bl_char = False
i = 0
for ch in name01:
i = 0
for ich in ch_name01:
bl_char = False
if ich[i][0] == ch:
bl_char = True
#ch_name01[i][1] = ch_name01[i][1] + 1
#print(str(ch_name01[i][0]) + " - " + str(ch_name01[i][1]))
if not bl_char:
i = i + 1
if bl_char == False:
ch_name01.append([ch, 1])
print(ch_name01)
#print(ch_name01)
for example input "aabbccddef"
would like to return a = 2, b = 2, c = 2, d = 2
but it will return error message
"TypeError: 'int' object is not subscriptable"
on if ich[i][0] == ch:
Using a dictionary will make your life so much easier here.
name01 = input("1st Name: ")
ch_name01 = {} # New dictionary
for ch in name01: # For every char in the input string
if ch in ch_name01: # If that char already has an entry in the dictionary
ch_name01[ch] += 1 # increment that char's entry's integer value
else: # the char hasn't been added to the dictionary yet
ch_name01[ch] = 1 # add a new dictionary entry with an integer value of 1
print(ch_name01)
Supplying input of aabbccddef produces:
{'a': 2, 'b': 2, 'c': 2, 'd': 2, 'e': 1, 'f': 1}
Edit:
I added code comments to explain what each line is doing
In Power Query M I am trying to create a recursive function that will turn a mess of multidimensional lists and records into one flat list of records, so that the records can be easily manipulated in PowerBI.
I have worked with recursion in other languages but I am quite new to using M.
The mess of lists and records is similar in structure to this:
Event
Event Details
Payments
Payment Details
There are some minor differences but they shouldn't matter.
I am hoping the output will be similar to this:
{
[event1, eventDetail1, payment1, paymentDetails1],
[event1, eventDetail1, payment1, paymentDetails2],
[event1, eventDetail1, payment1, paymentDetails3],
[event1, eventDetail1, payment2, paymentDetails1],
}
Continuing on for every single item.
This is the recursive function I have currently:
recursiveCollapse = (uncleanedList as list, eventCounter as number, paymentCounter as number, finalList as list) =>
let
eventLength = List.Count(uncleanedList),
firstIf = if eventCounter < eventLength then
let
secondIf = if paymentCounter < List.Count(uncleanedList{eventCounter}[eventPayments]) then
finalList = #recursiveCollapse(uncleanedList, eventCounter, paymentCounter + 1, finalList & {
[
EventName = uncleanedList{eventCounter}[eventDetailName],
EventDescription = uncleanedList{eventCounter}[eventDetailDescription],
EventSaleStatus = uncleanedList{eventCounter}[eventDetailStatus],
EventFirstDate = uncleanedList{eventCounter}[eventDetailFirst],
EventLastDate = uncleanedList{eventCounter}[eventDetailLast],
PaymentID = uncleanedList{eventCounter}[eventPaymentDetails][refs]{paymentCounter}[id],
PaymentName = uncleanedList{eventCounter}[eventPaymentDetails][refs]{paymentCounter}[name],
PaymentCreated = uncleanedList{eventCounter}[eventPayments]{paymentCounter}[paymentDetail][created],
CustomerEmail = uncleanedList{eventCounter}[eventPayments]{paymentCounter}[paymentDetail][customer][emailAddress],
CustomerFirstName = uncleanedList{eventCounter}[eventPayments]{paymentCounter}[paymentDetail][customer][firstName],
CustomerLastName = uncleanedList{eventCounter}[eventPayments]{paymentCounter}[paymentDetail][customer][lastName],
CustomerPhone = uncleanedList{eventCounter}[eventPayments]{paymentCounter}[paymentDetail][customer][mobilePhone],
PaymentStatus = uncleanedList{eventCounter}[eventPayments]{paymentCounter}[paymentDetail][status],
PaymentTotal = uncleanedList{eventCounter}[eventPayments]{paymentCounter}[paymentDetail][totalPrice][value]
]
})
else
finalList = #recursiveCollapse(uncleanedList, eventCounter + 1, 0, finalList)
in
finalList
else
finalList
in
finalList,
dataTable = recursiveCollapse(allEventsLinks, 0, 0, {})
in
dataTable
At this stage "dataTable" is just returned as an empty table.
I believe the problem is due to the "finalList" not being returned correctly through the recursive calls of the function. M does not have a return keyword, so I am lost on what to do from here.
Any help is appreciated.
Thanks
I figured it out.
To anyone else who needs help with this here is my solution:
recursiveCollapse = (uncleanedList as list, eventCounter as number, paymentCounter as
number, finalList as list) =>
let
returnList =
let
eventLength = List.Count(uncleanedList),
eventIf = if eventCounter < eventLength then
let
eventReturn = if paymentCounter + 1 < List.Count(uncleanedList{eventCounter}[eventPayments]) then
let
addRow =
finalList &
{
[
EventName = uncleanedList{eventCounter}[eventDetailName],
EventDescription = uncleanedList{eventCounter}[eventDetailDescription],
EventSaleStatus = uncleanedList{eventCounter}[eventDetailStatus],
EventFirstDate = uncleanedList{eventCounter}[eventDetailFirst],
EventLastDate = uncleanedList{eventCounter}[eventDetailLast],
PaymentID = uncleanedList{eventCounter}[eventPaymentDetails][refs]{paymentCounter + 1}[id],
PaymentName = uncleanedList{eventCounter}[eventPaymentDetails][refs]{paymentCounter + 1}[name],
PaymentCreated = uncleanedList{eventCounter}[eventPayments]{paymentCounter + 1}[paymentDetail][created],
CustomerEmail = uncleanedList{eventCounter}[eventPayments]{paymentCounter + 1}[paymentDetail][customer][emailAddress],
CustomerFirstName = uncleanedList{eventCounter}[eventPayments]{paymentCounter + 1}[paymentDetail][customer][firstName],
CustomerLastName = uncleanedList{eventCounter}[eventPayments]{paymentCounter + 1}[paymentDetail][customer][lastName],
CustomerPhone = uncleanedList{eventCounter}[eventPayments]{paymentCounter + 1}[paymentDetail][customer][mobilePhone],
PaymentStatus = uncleanedList{eventCounter}[eventPayments]{paymentCounter + 1}[paymentDetail][status],
PaymentTotal = uncleanedList{eventCounter}[eventPayments]{paymentCounter + 1}[paymentDetail][totalPrice][value]
]
},
recursion = #recursiveCollapse(uncleanedList, eventCounter, paymentCounter + 1, addRow)
in
recursion
else
let
recursion = #recursiveCollapse(uncleanedList, eventCounter + 1, 0, finalList)
in
recursion
in
eventReturn
else
finalList
in
eventIf
in
returnList,
dataTable = Table.FromList(recursiveCollapse(allEventsLinks, 0, 0, {}), Record.FieldValues, {
"EventName",
"EventDescription",
"EventSaleStatus",
"EventFirstDate",
"EventLastDate",
"PaymentID",
"PaymentName",
"PaymentCreated",
"CustomerEmail",
"CustomerFirstName",
"CustomerLastName",
"CustomerPhone",
"PaymentStatus",
"PaymentTotal"
})
in
dataTable
The following code is used to generate a table that an row can be added by a button, but only the data of the last row is eliminated after running.
import wx, wx.grid
class GridData(wx.grid.PyGridTableBase):
_cols = "a b c".split()
_data = [
"1 2 3".split(),
"4 5 6".split(),
"7 8 9".split()
]
_highlighted = set()
def GetColLabelValue(self, col):
return self._cols[col]
def GetNumberRows(self):
return len(self._data)
def GetNumberCols(self):
return len(self._cols)
def GetValue(self, row, col):
return self._data[row][col]
def SetValue(self, row, col, val):
self._data[row][col] = val
def AppendRows(self, *args):
msg = wx.grid.GridTableMessage(self,
wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED,
)
self.GetView().ProcessTableMessage(msg)
return True
# self.GetView().EndBatch()
# msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
# self.GetView().ProcessTableMessage(msg)
def GetAttr(self, row, col, kind):
attr = wx.grid.GridCellAttr()
attr.SetBackgroundColour(wx.GREEN if row in self._highlighted else wx.WHITE)
return attr
def set_value(self, row, col, val):
self._highlighted.add(row)
self.SetValue(row, col, val)
class Test(wx.Frame):#main frame
def __init__(self):
wx.Frame.__init__(self, None)
self.data = GridData()
self.grid = wx.grid.Grid(self)
self.grid.SetTable(self.data)
btn = wx.Button(self, label="set a2 to x")
btn.Bind(wx.EVT_BUTTON, self.OnTest)
self.Sizer = wx.BoxSizer(wx.VERTICAL)
self.Sizer.Add(self.grid, 1, wx.EXPAND)
self.Sizer.Add(btn, 0, wx.EXPAND)
def OnTest(self, event):
self.grid.AppendRows(numRows=3)
#self.data.set_value(1, 0, "x")
self.grid.Refresh()
app = wx.PySimpleApp()
app.TopWindow = Test()
app.TopWindow.Show()
app.MainLoop()
There is no error report,and the expectation can't be reached.
The following code is used to generate a table that can be added by a button, but only the data of the last row can be eliminated after running.
I am using Google Vision API, primarily to extract texts. I works fine, but for specific cases where I would need the API to scan the enter line, spits out the text before moving to the next line. However, it appears that the API is using some kind of logic that makes it scan top to bottom on the left side and moving to right side and doing a top to bottom scan. I would have liked if the API read left-to-right, move down and so on.
For example, consider the image:
The API returns the text like this:
“ Name DOB Gender: Lives In John Doe 01-Jan-1970 LA ”
Whereas, I would have expected something like this:
“ Name: John Doe DOB: 01-Jan-1970 Gender: M Lives In: LA ”
I suppose there is a way to define the block size or margin setting (?) to read the image/scan line by line?
Thanks for your help.
Alex
This might be a late answer but adding it for future reference.
You can add feature hints to your JSON request to get the desired results.
{
"requests": [
{
"image": {
"source": {
"imageUri": "https://i.stack.imgur.com/TRTXo.png"
}
},
"features": [
{
"type": "DOCUMENT_TEXT_DETECTION"
}
]
}
]
}
For text which are very far apart the DOCUMENT_TEXT_DETECTION also does not provide proper line segmentation.
The following code does simple line segmentation based on the character polygon coordinates.
https://github.com/sshniro/line-segmentation-algorithm-to-gcp-vision
Here a simple code to read line by line. y-axis for lines and x-axis for each word in the line.
items = []
lines = {}
for text in response.text_annotations[1:]:
top_x_axis = text.bounding_poly.vertices[0].x
top_y_axis = text.bounding_poly.vertices[0].y
bottom_y_axis = text.bounding_poly.vertices[3].y
if top_y_axis not in lines:
lines[top_y_axis] = [(top_y_axis, bottom_y_axis), []]
for s_top_y_axis, s_item in lines.items():
if top_y_axis < s_item[0][1]:
lines[s_top_y_axis][1].append((top_x_axis, text.description))
break
for _, item in lines.items():
if item[1]:
words = sorted(item[1], key=lambda t: t[0])
items.append((item[0], ' '.join([word for _, word in words]), words))
print(items)
You can extract the text based on the bounds per line too, you can use boundyPoly and concatenate the text in the same line
"boundingPoly": {
"vertices": [
{
"x": 87,
"y": 148
},
{
"x": 411,
"y": 148
},
{
"x": 411,
"y": 206
},
{
"x": 87,
"y": 206
}
]
for example this 2 words are in the same "line"
"description": "you",
"boundingPoly": {
"vertices": [
{
"x": 362,
"y": 1406
},
{
"x": 433,
"y": 1406
},
{
"x": 433,
"y": 1448
},
{
"x": 362,
"y": 1448
}
]
}
},
{
"description": "start",
"boundingPoly": {
"vertices": [
{
"x": 446,
"y": 1406
},
{
"x": 540,
"y": 1406
},
{
"x": 540,
"y": 1448
},
{
"x": 446,
"y": 1448
}
]
}
}
I get max and min y and iterate over y to get all potential lines, here is the full code
import io
import sys
from os import listdir
from google.cloud import vision
def read_image(image_file):
client = vision.ImageAnnotatorClient()
with io.open(image_file, "rb") as image_file:
content = image_file.read()
image = vision.Image(content=content)
return client.document_text_detection(
image=image,
image_context={"language_hints": ["bg"]}
)
def extract_paragraphs(image_file):
response = read_image(image_file)
min_y = sys.maxsize
max_y = -1
for t in response.text_annotations:
poly_range = get_poly_y_range(t.bounding_poly)
t_min = min(poly_range)
t_max = max(poly_range)
if t_min < min_y:
min_y = t_min
if t_max > max_y:
max_y = t_max
max_size = max_y - min_y
text_boxes = []
for t in response.text_annotations:
poly_range = get_poly_y_range(t.bounding_poly)
t_x = get_poly_x(t.bounding_poly)
t_min = min(poly_range)
t_max = max(poly_range)
poly_size = t_max - t_min
text_boxes.append({
'min_y': t_min,
'max_y': t_max,
'x': t_x,
'size': poly_size,
'description': t.description
})
paragraphs = []
for i in range(min_y, max_y):
para_line = []
for text_box in text_boxes:
t_min = text_box['min_y']
t_max = text_box['max_y']
x = text_box['x']
size = text_box['size']
# size < max_size excludes the biggest rect
if size < max_size * 0.9 and t_min <= i <= t_max:
para_line.append(
{
'text': text_box['description'],
'x': x
}
)
# here I have to sort them by x so the don't get randomly shuffled
para_line = sorted(para_line, key=lambda x: x['x'])
line = " ".join(map(lambda x: x['text'], para_line))
paragraphs.append(line)
# if line not in paragraphs:
# paragraphs.append(line)
return "\n".join(paragraphs)
def get_poly_y_range(poly):
y_list = []
for v in poly.vertices:
if v.y not in y_list:
y_list.append(v.y)
return y_list
def get_poly_x(poly):
return poly.vertices[0].x
def extract_paragraphs_from_image(picName):
print(picName)
pic_path = rootPics + "/" + picName
text = extract_paragraphs(pic_path)
text_path = outputRoot + "/" + picName + ".txt"
write(text_path, text)
This code is WIP.
In the end, I get the same line multiple times and post-processing to determine the exact values. (paragraphs variable). Let me know if I have to clarify anything
Inspired by Borislav's answer, I just wrote something for python that also works for handwriting. It's messy and I am new to python, but I think you can get an idea of how to do this.
A class to hold some extended data for each word, for example, the average y position of a word, which I used to calculate the differences between words:
import re
from operator import attrgetter
import numpy as np
class ExtendedAnnotation:
def __init__(self, annotation):
self.vertex = annotation.bounding_poly.vertices
self.text = annotation.description
self.avg_y = (self.vertex[0].y + self.vertex[1].y + self.vertex[2].y + self.vertex[3].y) / 4
self.height = ((self.vertex[3].y - self.vertex[1].y) + (self.vertex[2].y - self.vertex[0].y)) / 2
self.start_x = (self.vertex[0].x + self.vertex[3].x) / 2
def __repr__(self):
return '{' + self.text + ', ' + str(self.avg_y) + ', ' + str(self.height) + ', ' + str(self.start_x) + '}'
Create objects with that data:
def get_extended_annotations(response):
extended_annotations = []
for annotation in response.text_annotations:
extended_annotations.append(ExtendedAnnotation(annotation))
# delete last item, as it is the whole text I guess.
del extended_annotations[0]
return extended_annotations
Calculate the threshold.
First, all words a sorted by their y position, defined as being the average of all 4 corners of a word. The x position is not relevant at this moment.
Then, the differences between every word and their following word are calculated. For a perfectly straight line of words, you would expect the differences of the y position between every two words to be 0. Even for handwriting, it should be around 1 ~ 10.
However, whenever there is a line break, the difference between the last word of the former row and the first word of the new row is much greater than that, for example, 50 or 60.
So to decide whether there should be a line break between two words, the standard deviation of the differences is used.
def get_threshold_for_y_difference(annotations):
annotations.sort(key=attrgetter('avg_y'))
differences = []
for i in range(0, len(annotations)):
if i == 0:
continue
differences.append(abs(annotations[i].avg_y - annotations[i - 1].avg_y))
return np.std(differences)
Having calculated the threshold, the list of all words gets grouped into rows accordingly.
def group_annotations(annotations, threshold):
annotations.sort(key=attrgetter('avg_y'))
line_index = 0
text = [[]]
for i in range(0, len(annotations)):
if i == 0:
text[line_index].append(annotations[i])
continue
y_difference = abs(annotations[i].avg_y - annotations[i - 1].avg_y)
if y_difference > threshold:
line_index = line_index + 1
text.append([])
text[line_index].append(annotations[i])
return text
Finally, each row is sorted by their x position to get them into the correct order from left to right.
Then a little regex is used to remove whitespace in front of interpunctuation.
def sort_and_combine_grouped_annotations(annotation_lists):
grouped_list = []
for annotation_group in annotation_lists:
annotation_group.sort(key=attrgetter('start_x'))
texts = (o.text for o in annotation_group)
texts = ' '.join(texts)
texts = re.sub(r'\s([-;:?.!](?:\s|$))', r'\1', texts)
grouped_list.append(texts)
return grouped_list
Based on Borislav Stoilov latest answer I wrote the code for c# for anybody that might need it in the future. Find the code bellow:
public static List<TextParagraph> ExtractParagraphs(IReadOnlyList<EntityAnnotation> textAnnotations)
{
var min_y = int.MaxValue;
var max_y = -1;
foreach (var item in textAnnotations)
{
var poly_range = Get_poly_y_range(item.BoundingPoly);
var t_min = poly_range.Min();
var t_max = poly_range.Max();
if (t_min < min_y) min_y = t_min;
if (t_max > max_y) max_y = t_max;
}
var max_size = max_y - min_y;
var text_boxes = new List<TextBox>();
foreach (var item in textAnnotations)
{
var poly_range = Get_poly_y_range(item.BoundingPoly);
var t_x = Get_poly_x(item.BoundingPoly);
var t_min = poly_range.Min();
var t_max = poly_range.Max();
var poly_size = t_max - t_min;
text_boxes.Add(new TextBox
{
Min_y = t_min,
Max_y = t_max,
X = t_x,
Size = poly_size,
Description = item.Description
});
}
var paragraphs = new List<TextParagraph>();
for (int i = min_y; i < max_y; i++)
{
var para_line = new List<TextLine>();
foreach (var text_box in text_boxes)
{
int t_min = text_box.Min_y;
int t_max = text_box.Max_y;
int x = text_box.X;
int size = text_box.Size;
//# size < max_size excludes the biggest rect
if (size < (max_size * 0.9) && t_min <= i && i <= t_max)
para_line.Add(
new TextLine
{
Text = text_box.Description,
X = x
}
);
}
// here I have to sort them by x so the don't get randomly enter code hereshuffled
para_line = para_line.OrderBy(x => x.X).ToList();
var line = string.Join(" ", para_line.Select(x => x.Text));
var paragraph = new TextParagraph
{
Order = i,
Text = line,
WordCount = para_line.Count,
TextBoxes = para_line
};
paragraphs.Add(paragraph);
}
return paragraphs;
//return string.Join("\n", paragraphs);
}
private static List<int> Get_poly_y_range(BoundingPoly poly)
{
var y_list = new List<int>();
foreach (var v in poly.Vertices)
{
if (!y_list.Contains(v.Y))
{
y_list.Add(v.Y);
}
}
return y_list;
}
private static int Get_poly_x(BoundingPoly poly)
{
return poly.Vertices[0].X;
}
Calling ExtractParagraphs() method will return a list of strings which contains doubles from the file. I also wrote some custom code to treat that problem. If you need any help processing the doubles let me know, and I could provide the rest of the code.
Example:
Text in picture: "I want to make this thing work 24/7!"
Code will return:
"I"
"I want"
"I want to "
"I want to make"
"I want to make this"
"I want to make this thing"
"I want to make this thing work"
"I want to make this thing work 24/7!"
"to make this thing work 24/7!"
"this thing work 24/7!"
"thing work 24/7!"
"work 24/7!"
"24/7!"
I also have an implementation of parsing PDFs to PNGs beacause Google Cloud Vision Api won't accept PDFs that are not stored in the Cloud Bucket. If needed I can provide it.
Happy coding!